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