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 }