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