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 }