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.security.PrivilegedExceptionAction;
027    import java.security.Security;
028    import java.util.Map;
029    import java.util.TreeMap;
030    
031    import javax.security.auth.callback.Callback;
032    import javax.security.auth.callback.CallbackHandler;
033    import javax.security.auth.callback.NameCallback;
034    import javax.security.auth.callback.PasswordCallback;
035    import javax.security.auth.callback.UnsupportedCallbackException;
036    import javax.security.sasl.AuthorizeCallback;
037    import javax.security.sasl.RealmCallback;
038    import javax.security.sasl.Sasl;
039    import javax.security.sasl.SaslException;
040    import javax.security.sasl.SaslServer;
041    
042    import org.apache.commons.codec.binary.Base64;
043    import org.apache.commons.logging.Log;
044    import org.apache.commons.logging.LogFactory;
045    import org.apache.hadoop.classification.InterfaceAudience;
046    import org.apache.hadoop.classification.InterfaceStability;
047    import org.apache.hadoop.conf.Configuration;
048    import org.apache.hadoop.ipc.Server;
049    import org.apache.hadoop.ipc.Server.Connection;
050    import org.apache.hadoop.security.token.SecretManager;
051    import org.apache.hadoop.security.token.TokenIdentifier;
052    import org.apache.hadoop.security.token.SecretManager.InvalidToken;
053    
054    /**
055     * A utility class for dealing with SASL on RPC server
056     */
057    @InterfaceAudience.LimitedPrivate({"HDFS", "MapReduce"})
058    @InterfaceStability.Evolving
059    public class SaslRpcServer {
060      public static final Log LOG = LogFactory.getLog(SaslRpcServer.class);
061      public static final String SASL_DEFAULT_REALM = "default";
062      public static final Map<String, String> SASL_PROPS = 
063          new TreeMap<String, String>();
064    
065      public static enum QualityOfProtection {
066        AUTHENTICATION("auth"),
067        INTEGRITY("auth-int"),
068        PRIVACY("auth-conf");
069        
070        public final String saslQop;
071        
072        private QualityOfProtection(String saslQop) {
073          this.saslQop = saslQop;
074        }
075        
076        public String getSaslQop() {
077          return saslQop;
078        }
079      }
080    
081      @InterfaceAudience.Private
082      @InterfaceStability.Unstable
083      public AuthMethod authMethod;
084      public String mechanism;
085      public String protocol;
086      public String serverId;
087      
088      @InterfaceAudience.Private
089      @InterfaceStability.Unstable
090      public SaslRpcServer(AuthMethod authMethod) throws IOException {
091        this.authMethod = authMethod;
092        mechanism = authMethod.getMechanismName();    
093        switch (authMethod) {
094          case SIMPLE: {
095            return; // no sasl for simple
096          }
097          case TOKEN: {
098            protocol = "";
099            serverId = SaslRpcServer.SASL_DEFAULT_REALM;
100            break;
101          }
102          case KERBEROS: {
103            String fullName = UserGroupInformation.getCurrentUser().getUserName();
104            if (LOG.isDebugEnabled())
105              LOG.debug("Kerberos principal name is " + fullName);
106            // don't use KerberosName because we don't want auth_to_local
107            String[] parts = fullName.split("[/@]", 3);
108            protocol = parts[0];
109            // should verify service host is present here rather than in create()
110            // but lazy tests are using a UGI that isn't a SPN...
111            serverId = (parts.length < 2) ? "" : parts[1];
112            break;
113          }
114          default:
115            // we should never be able to get here
116            throw new AccessControlException(
117                "Server does not support SASL " + authMethod);
118        }
119      }
120      
121      @InterfaceAudience.Private
122      @InterfaceStability.Unstable
123      public SaslServer create(Connection connection,
124                               SecretManager<TokenIdentifier> secretManager
125          ) throws IOException, InterruptedException {
126        UserGroupInformation ugi = UserGroupInformation.getCurrentUser();
127        final CallbackHandler callback;
128        switch (authMethod) {
129          case TOKEN: {
130            callback = new SaslDigestCallbackHandler(secretManager, connection);
131            break;
132          }
133          case KERBEROS: {
134            if (serverId.isEmpty()) {
135              throw new AccessControlException(
136                  "Kerberos principal name does NOT have the expected "
137                      + "hostname part: " + ugi.getUserName());
138            }
139            callback = new SaslGssCallbackHandler();
140            break;
141          }
142          default:
143            // we should never be able to get here
144            throw new AccessControlException(
145                "Server does not support SASL " + authMethod);
146        }
147        
148        SaslServer saslServer = ugi.doAs(
149            new PrivilegedExceptionAction<SaslServer>() {
150              @Override
151              public SaslServer run() throws SaslException  {
152                return Sasl.createSaslServer(mechanism, protocol, serverId,
153                    SaslRpcServer.SASL_PROPS, callback);
154              }
155            });
156        if (saslServer == null) {
157          throw new AccessControlException(
158              "Unable to find SASL server implementation for " + mechanism);
159        }
160        if (LOG.isDebugEnabled()) {
161          LOG.debug("Created SASL server with mechanism = " + mechanism);
162        }
163        return saslServer;
164      }
165    
166      public static void init(Configuration conf) {
167        QualityOfProtection saslQOP = QualityOfProtection.AUTHENTICATION;
168        String rpcProtection = conf.get("hadoop.rpc.protection",
169            QualityOfProtection.AUTHENTICATION.name().toLowerCase());
170        if (QualityOfProtection.INTEGRITY.name().toLowerCase()
171            .equals(rpcProtection)) {
172          saslQOP = QualityOfProtection.INTEGRITY;
173        } else if (QualityOfProtection.PRIVACY.name().toLowerCase().equals(
174            rpcProtection)) {
175          saslQOP = QualityOfProtection.PRIVACY;
176        }
177        
178        SASL_PROPS.put(Sasl.QOP, saslQOP.getSaslQop());
179        SASL_PROPS.put(Sasl.SERVER_AUTH, "true");
180        Security.addProvider(new SaslPlainServer.SecurityProvider());
181      }
182      
183      static String encodeIdentifier(byte[] identifier) {
184        return new String(Base64.encodeBase64(identifier));
185      }
186    
187      static byte[] decodeIdentifier(String identifier) {
188        return Base64.decodeBase64(identifier.getBytes());
189      }
190    
191      public static <T extends TokenIdentifier> T getIdentifier(String id,
192          SecretManager<T> secretManager) throws InvalidToken {
193        byte[] tokenId = decodeIdentifier(id);
194        T tokenIdentifier = secretManager.createIdentifier();
195        try {
196          tokenIdentifier.readFields(new DataInputStream(new ByteArrayInputStream(
197              tokenId)));
198        } catch (IOException e) {
199          throw (InvalidToken) new InvalidToken(
200              "Can't de-serialize tokenIdentifier").initCause(e);
201        }
202        return tokenIdentifier;
203      }
204    
205      static char[] encodePassword(byte[] password) {
206        return new String(Base64.encodeBase64(password)).toCharArray();
207      }
208    
209      /** Splitting fully qualified Kerberos name into parts */
210      public static String[] splitKerberosName(String fullName) {
211        return fullName.split("[/@]");
212      }
213    
214      /** Authentication method */
215      @InterfaceStability.Evolving
216      public static enum AuthMethod {
217        SIMPLE((byte) 80, ""),
218        KERBEROS((byte) 81, "GSSAPI"),
219        @Deprecated
220        DIGEST((byte) 82, "DIGEST-MD5"),
221        TOKEN((byte) 82, "DIGEST-MD5"),
222        PLAIN((byte) 83, "PLAIN");
223    
224        /** The code for this method. */
225        public final byte code;
226        public final String mechanismName;
227    
228        private AuthMethod(byte code, String mechanismName) { 
229          this.code = code;
230          this.mechanismName = mechanismName;
231        }
232    
233        private static final int FIRST_CODE = values()[0].code;
234    
235        /** Return the object represented by the code. */
236        private static AuthMethod valueOf(byte code) {
237          final int i = (code & 0xff) - FIRST_CODE;
238          return i < 0 || i >= values().length ? null : values()[i];
239        }
240    
241        /** Return the SASL mechanism name */
242        public String getMechanismName() {
243          return mechanismName;
244        }
245    
246        /** Read from in */
247        public static AuthMethod read(DataInput in) throws IOException {
248          return valueOf(in.readByte());
249        }
250    
251        /** Write to out */
252        public void write(DataOutput out) throws IOException {
253          out.write(code);
254        }
255      };
256    
257      /** CallbackHandler for SASL DIGEST-MD5 mechanism */
258      @InterfaceStability.Evolving
259      public static class SaslDigestCallbackHandler implements CallbackHandler {
260        private SecretManager<TokenIdentifier> secretManager;
261        private Server.Connection connection; 
262        
263        public SaslDigestCallbackHandler(
264            SecretManager<TokenIdentifier> secretManager,
265            Server.Connection connection) {
266          this.secretManager = secretManager;
267          this.connection = connection;
268        }
269    
270        private char[] getPassword(TokenIdentifier tokenid) throws InvalidToken {
271          return encodePassword(secretManager.retrievePassword(tokenid));
272        }
273    
274        @Override
275        public void handle(Callback[] callbacks) throws InvalidToken,
276            UnsupportedCallbackException {
277          NameCallback nc = null;
278          PasswordCallback pc = null;
279          AuthorizeCallback ac = null;
280          for (Callback callback : callbacks) {
281            if (callback instanceof AuthorizeCallback) {
282              ac = (AuthorizeCallback) callback;
283            } else if (callback instanceof NameCallback) {
284              nc = (NameCallback) callback;
285            } else if (callback instanceof PasswordCallback) {
286              pc = (PasswordCallback) callback;
287            } else if (callback instanceof RealmCallback) {
288              continue; // realm is ignored
289            } else {
290              throw new UnsupportedCallbackException(callback,
291                  "Unrecognized SASL DIGEST-MD5 Callback");
292            }
293          }
294          if (pc != null) {
295            TokenIdentifier tokenIdentifier = getIdentifier(nc.getDefaultName(), secretManager);
296            char[] password = getPassword(tokenIdentifier);
297            UserGroupInformation user = null;
298            user = tokenIdentifier.getUser(); // may throw exception
299            connection.attemptingUser = user;
300            
301            if (LOG.isDebugEnabled()) {
302              LOG.debug("SASL server DIGEST-MD5 callback: setting password "
303                  + "for client: " + tokenIdentifier.getUser());
304            }
305            pc.setPassword(password);
306          }
307          if (ac != null) {
308            String authid = ac.getAuthenticationID();
309            String authzid = ac.getAuthorizationID();
310            if (authid.equals(authzid)) {
311              ac.setAuthorized(true);
312            } else {
313              ac.setAuthorized(false);
314            }
315            if (ac.isAuthorized()) {
316              if (LOG.isDebugEnabled()) {
317                String username =
318                  getIdentifier(authzid, secretManager).getUser().getUserName();
319                LOG.debug("SASL server DIGEST-MD5 callback: setting "
320                    + "canonicalized client ID: " + username);
321              }
322              ac.setAuthorizedID(authzid);
323            }
324          }
325        }
326      }
327    
328      /** CallbackHandler for SASL GSSAPI Kerberos mechanism */
329      @InterfaceStability.Evolving
330      public static class SaslGssCallbackHandler implements CallbackHandler {
331    
332        @Override
333        public void handle(Callback[] callbacks) throws
334            UnsupportedCallbackException {
335          AuthorizeCallback ac = null;
336          for (Callback callback : callbacks) {
337            if (callback instanceof AuthorizeCallback) {
338              ac = (AuthorizeCallback) callback;
339            } else {
340              throw new UnsupportedCallbackException(callback,
341                  "Unrecognized SASL GSSAPI Callback");
342            }
343          }
344          if (ac != null) {
345            String authid = ac.getAuthenticationID();
346            String authzid = ac.getAuthorizationID();
347            if (authid.equals(authzid)) {
348              ac.setAuthorized(true);
349            } else {
350              ac.setAuthorized(false);
351            }
352            if (ac.isAuthorized()) {
353              if (LOG.isDebugEnabled())
354                LOG.debug("SASL server GSSAPI callback: setting "
355                    + "canonicalized client ID: " + authzid);
356              ac.setAuthorizedID(authzid);
357            }
358          }
359        }
360      }
361    }