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.util.Map;
027 import java.util.TreeMap;
028
029 import javax.security.auth.callback.Callback;
030 import javax.security.auth.callback.CallbackHandler;
031 import javax.security.auth.callback.NameCallback;
032 import javax.security.auth.callback.PasswordCallback;
033 import javax.security.auth.callback.UnsupportedCallbackException;
034 import javax.security.sasl.AuthorizeCallback;
035 import javax.security.sasl.RealmCallback;
036 import javax.security.sasl.Sasl;
037
038 import org.apache.commons.codec.binary.Base64;
039 import org.apache.commons.logging.Log;
040 import org.apache.commons.logging.LogFactory;
041 import org.apache.hadoop.classification.InterfaceAudience;
042 import org.apache.hadoop.classification.InterfaceStability;
043 import org.apache.hadoop.conf.Configuration;
044 import org.apache.hadoop.ipc.Server;
045 import org.apache.hadoop.security.UserGroupInformation.AuthenticationMethod;
046 import org.apache.hadoop.security.token.SecretManager;
047 import org.apache.hadoop.security.token.TokenIdentifier;
048 import org.apache.hadoop.security.token.SecretManager.InvalidToken;
049
050 /**
051 * A utility class for dealing with SASL on RPC server
052 */
053 @InterfaceAudience.LimitedPrivate({"HDFS", "MapReduce"})
054 @InterfaceStability.Evolving
055 public class SaslRpcServer {
056 public static final Log LOG = LogFactory.getLog(SaslRpcServer.class);
057 public static final String SASL_DEFAULT_REALM = "default";
058 public static final Map<String, String> SASL_PROPS =
059 new TreeMap<String, String>();
060
061 public static final int SWITCH_TO_SIMPLE_AUTH = -88;
062
063 public static enum QualityOfProtection {
064 AUTHENTICATION("auth"),
065 INTEGRITY("auth-int"),
066 PRIVACY("auth-conf");
067
068 public final String saslQop;
069
070 private QualityOfProtection(String saslQop) {
071 this.saslQop = saslQop;
072 }
073
074 public String getSaslQop() {
075 return saslQop;
076 }
077 }
078
079 public static void init(Configuration conf) {
080 QualityOfProtection saslQOP = QualityOfProtection.AUTHENTICATION;
081 String rpcProtection = conf.get("hadoop.rpc.protection",
082 QualityOfProtection.AUTHENTICATION.name().toLowerCase());
083 if (QualityOfProtection.INTEGRITY.name().toLowerCase()
084 .equals(rpcProtection)) {
085 saslQOP = QualityOfProtection.INTEGRITY;
086 } else if (QualityOfProtection.PRIVACY.name().toLowerCase().equals(
087 rpcProtection)) {
088 saslQOP = QualityOfProtection.PRIVACY;
089 }
090
091 SASL_PROPS.put(Sasl.QOP, saslQOP.getSaslQop());
092 SASL_PROPS.put(Sasl.SERVER_AUTH, "true");
093 }
094
095 static String encodeIdentifier(byte[] identifier) {
096 return new String(Base64.encodeBase64(identifier));
097 }
098
099 static byte[] decodeIdentifier(String identifier) {
100 return Base64.decodeBase64(identifier.getBytes());
101 }
102
103 public static <T extends TokenIdentifier> T getIdentifier(String id,
104 SecretManager<T> secretManager) throws InvalidToken {
105 byte[] tokenId = decodeIdentifier(id);
106 T tokenIdentifier = secretManager.createIdentifier();
107 try {
108 tokenIdentifier.readFields(new DataInputStream(new ByteArrayInputStream(
109 tokenId)));
110 } catch (IOException e) {
111 throw (InvalidToken) new InvalidToken(
112 "Can't de-serialize tokenIdentifier").initCause(e);
113 }
114 return tokenIdentifier;
115 }
116
117 static char[] encodePassword(byte[] password) {
118 return new String(Base64.encodeBase64(password)).toCharArray();
119 }
120
121 /** Splitting fully qualified Kerberos name into parts */
122 public static String[] splitKerberosName(String fullName) {
123 return fullName.split("[/@]");
124 }
125
126 @InterfaceStability.Evolving
127 public enum SaslStatus {
128 SUCCESS (0),
129 ERROR (1);
130
131 public final int state;
132 private SaslStatus(int state) {
133 this.state = state;
134 }
135 }
136
137 /** Authentication method */
138 @InterfaceStability.Evolving
139 public static enum AuthMethod {
140 SIMPLE((byte) 80, "", AuthenticationMethod.SIMPLE),
141 KERBEROS((byte) 81, "GSSAPI", AuthenticationMethod.KERBEROS),
142 DIGEST((byte) 82, "DIGEST-MD5", AuthenticationMethod.TOKEN);
143
144 /** The code for this method. */
145 public final byte code;
146 public final String mechanismName;
147 public final AuthenticationMethod authenticationMethod;
148
149 private AuthMethod(byte code, String mechanismName,
150 AuthenticationMethod authMethod) {
151 this.code = code;
152 this.mechanismName = mechanismName;
153 this.authenticationMethod = authMethod;
154 }
155
156 private static final int FIRST_CODE = values()[0].code;
157
158 /** Return the object represented by the code. */
159 private static AuthMethod valueOf(byte code) {
160 final int i = (code & 0xff) - FIRST_CODE;
161 return i < 0 || i >= values().length ? null : values()[i];
162 }
163
164 /** Return the SASL mechanism name */
165 public String getMechanismName() {
166 return mechanismName;
167 }
168
169 /** Read from in */
170 public static AuthMethod read(DataInput in) throws IOException {
171 return valueOf(in.readByte());
172 }
173
174 /** Write to out */
175 public void write(DataOutput out) throws IOException {
176 out.write(code);
177 }
178 };
179
180 /** CallbackHandler for SASL DIGEST-MD5 mechanism */
181 @InterfaceStability.Evolving
182 public static class SaslDigestCallbackHandler implements CallbackHandler {
183 private SecretManager<TokenIdentifier> secretManager;
184 private Server.Connection connection;
185
186 public SaslDigestCallbackHandler(
187 SecretManager<TokenIdentifier> secretManager,
188 Server.Connection connection) {
189 this.secretManager = secretManager;
190 this.connection = connection;
191 }
192
193 private char[] getPassword(TokenIdentifier tokenid) throws InvalidToken {
194 return encodePassword(secretManager.retrievePassword(tokenid));
195 }
196
197 /** {@inheritDoc} */
198 @Override
199 public void handle(Callback[] callbacks) throws InvalidToken,
200 UnsupportedCallbackException {
201 NameCallback nc = null;
202 PasswordCallback pc = null;
203 AuthorizeCallback ac = null;
204 for (Callback callback : callbacks) {
205 if (callback instanceof AuthorizeCallback) {
206 ac = (AuthorizeCallback) callback;
207 } else if (callback instanceof NameCallback) {
208 nc = (NameCallback) callback;
209 } else if (callback instanceof PasswordCallback) {
210 pc = (PasswordCallback) callback;
211 } else if (callback instanceof RealmCallback) {
212 continue; // realm is ignored
213 } else {
214 throw new UnsupportedCallbackException(callback,
215 "Unrecognized SASL DIGEST-MD5 Callback");
216 }
217 }
218 if (pc != null) {
219 TokenIdentifier tokenIdentifier = getIdentifier(nc.getDefaultName(), secretManager);
220 char[] password = getPassword(tokenIdentifier);
221 UserGroupInformation user = null;
222 user = tokenIdentifier.getUser(); // may throw exception
223 connection.attemptingUser = user;
224
225 if (LOG.isDebugEnabled()) {
226 LOG.debug("SASL server DIGEST-MD5 callback: setting password "
227 + "for client: " + tokenIdentifier.getUser());
228 }
229 pc.setPassword(password);
230 }
231 if (ac != null) {
232 String authid = ac.getAuthenticationID();
233 String authzid = ac.getAuthorizationID();
234 if (authid.equals(authzid)) {
235 ac.setAuthorized(true);
236 } else {
237 ac.setAuthorized(false);
238 }
239 if (ac.isAuthorized()) {
240 if (LOG.isDebugEnabled()) {
241 String username =
242 getIdentifier(authzid, secretManager).getUser().getUserName();
243 LOG.debug("SASL server DIGEST-MD5 callback: setting "
244 + "canonicalized client ID: " + username);
245 }
246 ac.setAuthorizedID(authzid);
247 }
248 }
249 }
250 }
251
252 /** CallbackHandler for SASL GSSAPI Kerberos mechanism */
253 @InterfaceStability.Evolving
254 public static class SaslGssCallbackHandler implements CallbackHandler {
255
256 /** {@inheritDoc} */
257 @Override
258 public void handle(Callback[] callbacks) throws
259 UnsupportedCallbackException {
260 AuthorizeCallback ac = null;
261 for (Callback callback : callbacks) {
262 if (callback instanceof AuthorizeCallback) {
263 ac = (AuthorizeCallback) callback;
264 } else {
265 throw new UnsupportedCallbackException(callback,
266 "Unrecognized SASL GSSAPI Callback");
267 }
268 }
269 if (ac != null) {
270 String authid = ac.getAuthenticationID();
271 String authzid = ac.getAuthorizationID();
272 if (authid.equals(authzid)) {
273 ac.setAuthorized(true);
274 } else {
275 ac.setAuthorized(false);
276 }
277 if (ac.isAuthorized()) {
278 if (LOG.isDebugEnabled())
279 LOG.debug("SASL server GSSAPI callback: setting "
280 + "canonicalized client ID: " + authzid);
281 ac.setAuthorizedID(authzid);
282 }
283 }
284 }
285 }
286 }