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.authentication.util.KerberosName;
051 import org.apache.hadoop.security.token.SecretManager;
052 import org.apache.hadoop.security.token.TokenIdentifier;
053 import org.apache.hadoop.security.token.SecretManager.InvalidToken;
054
055 /**
056 * A utility class for dealing with SASL on RPC server
057 */
058 @InterfaceAudience.LimitedPrivate({"HDFS", "MapReduce"})
059 @InterfaceStability.Evolving
060 public class SaslRpcServer {
061 public static final Log LOG = LogFactory.getLog(SaslRpcServer.class);
062 public static final String SASL_DEFAULT_REALM = "default";
063 public static final Map<String, String> SASL_PROPS =
064 new TreeMap<String, String>();
065
066 public static enum QualityOfProtection {
067 AUTHENTICATION("auth"),
068 INTEGRITY("auth-int"),
069 PRIVACY("auth-conf");
070
071 public final String saslQop;
072
073 private QualityOfProtection(String saslQop) {
074 this.saslQop = saslQop;
075 }
076
077 public String getSaslQop() {
078 return saslQop;
079 }
080 }
081
082 @InterfaceAudience.Private
083 @InterfaceStability.Unstable
084 public AuthMethod authMethod;
085 public String mechanism;
086 public String protocol;
087 public String serverId;
088
089 @InterfaceAudience.Private
090 @InterfaceStability.Unstable
091 public SaslRpcServer(AuthMethod authMethod) throws IOException {
092 this.authMethod = authMethod;
093 mechanism = authMethod.getMechanismName();
094 switch (authMethod) {
095 case SIMPLE: {
096 return; // no sasl for simple
097 }
098 case TOKEN: {
099 protocol = "";
100 serverId = SaslRpcServer.SASL_DEFAULT_REALM;
101 break;
102 }
103 case KERBEROS: {
104 String fullName = UserGroupInformation.getCurrentUser().getUserName();
105 if (LOG.isDebugEnabled())
106 LOG.debug("Kerberos principal name is " + fullName);
107 KerberosName krbName = new KerberosName(fullName);
108 serverId = krbName.getHostName();
109 if (serverId == null) {
110 serverId = "";
111 }
112 protocol = krbName.getServiceName();
113 break;
114 }
115 default:
116 // we should never be able to get here
117 throw new AccessControlException(
118 "Server does not support SASL " + authMethod);
119 }
120 }
121
122 @InterfaceAudience.Private
123 @InterfaceStability.Unstable
124 public SaslServer create(Connection connection,
125 SecretManager<TokenIdentifier> secretManager
126 ) throws IOException, InterruptedException {
127 UserGroupInformation ugi = UserGroupInformation.getCurrentUser();
128 final CallbackHandler callback;
129 switch (authMethod) {
130 case TOKEN: {
131 secretManager.checkAvailableForRead();
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 return encodePassword(secretManager.retrievePassword(tokenid));
274 }
275
276 @Override
277 public void handle(Callback[] callbacks) throws InvalidToken,
278 UnsupportedCallbackException {
279 NameCallback nc = null;
280 PasswordCallback pc = null;
281 AuthorizeCallback ac = null;
282 for (Callback callback : callbacks) {
283 if (callback instanceof AuthorizeCallback) {
284 ac = (AuthorizeCallback) callback;
285 } else if (callback instanceof NameCallback) {
286 nc = (NameCallback) callback;
287 } else if (callback instanceof PasswordCallback) {
288 pc = (PasswordCallback) callback;
289 } else if (callback instanceof RealmCallback) {
290 continue; // realm is ignored
291 } else {
292 throw new UnsupportedCallbackException(callback,
293 "Unrecognized SASL DIGEST-MD5 Callback");
294 }
295 }
296 if (pc != null) {
297 TokenIdentifier tokenIdentifier = getIdentifier(nc.getDefaultName(), secretManager);
298 char[] password = getPassword(tokenIdentifier);
299 UserGroupInformation user = null;
300 user = tokenIdentifier.getUser(); // may throw exception
301 connection.attemptingUser = user;
302
303 if (LOG.isDebugEnabled()) {
304 LOG.debug("SASL server DIGEST-MD5 callback: setting password "
305 + "for client: " + tokenIdentifier.getUser());
306 }
307 pc.setPassword(password);
308 }
309 if (ac != null) {
310 String authid = ac.getAuthenticationID();
311 String authzid = ac.getAuthorizationID();
312 if (authid.equals(authzid)) {
313 ac.setAuthorized(true);
314 } else {
315 ac.setAuthorized(false);
316 }
317 if (ac.isAuthorized()) {
318 if (LOG.isDebugEnabled()) {
319 String username =
320 getIdentifier(authzid, secretManager).getUser().getUserName();
321 LOG.debug("SASL server DIGEST-MD5 callback: setting "
322 + "canonicalized client ID: " + username);
323 }
324 ac.setAuthorizedID(authzid);
325 }
326 }
327 }
328 }
329
330 /** CallbackHandler for SASL GSSAPI Kerberos mechanism */
331 @InterfaceStability.Evolving
332 public static class SaslGssCallbackHandler implements CallbackHandler {
333
334 @Override
335 public void handle(Callback[] callbacks) throws
336 UnsupportedCallbackException {
337 AuthorizeCallback ac = null;
338 for (Callback callback : callbacks) {
339 if (callback instanceof AuthorizeCallback) {
340 ac = (AuthorizeCallback) callback;
341 } else {
342 throw new UnsupportedCallbackException(callback,
343 "Unrecognized SASL GSSAPI Callback");
344 }
345 }
346 if (ac != null) {
347 String authid = ac.getAuthenticationID();
348 String authzid = ac.getAuthorizationID();
349 if (authid.equals(authzid)) {
350 ac.setAuthorized(true);
351 } else {
352 ac.setAuthorized(false);
353 }
354 if (ac.isAuthorized()) {
355 if (LOG.isDebugEnabled())
356 LOG.debug("SASL server GSSAPI callback: setting "
357 + "canonicalized client ID: " + authzid);
358 ac.setAuthorizedID(authzid);
359 }
360 }
361 }
362 }
363 }