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 019package org.apache.hadoop.hbase.thrift; 020 021import java.io.IOException; 022import java.security.PrivilegedExceptionAction; 023import java.util.Base64; 024 025import javax.servlet.ServletException; 026import javax.servlet.http.HttpServletRequest; 027import javax.servlet.http.HttpServletResponse; 028 029import org.apache.hadoop.hbase.security.SecurityUtil; 030import org.apache.hadoop.security.UserGroupInformation; 031import org.apache.hadoop.security.authorize.AuthorizationException; 032import org.apache.hadoop.security.authorize.ProxyUsers; 033import org.apache.http.HttpHeaders; 034import org.apache.thrift.TProcessor; 035import org.apache.thrift.protocol.TProtocolFactory; 036import org.apache.thrift.server.TServlet; 037import org.apache.yetus.audience.InterfaceAudience; 038import org.ietf.jgss.GSSContext; 039import org.ietf.jgss.GSSCredential; 040import org.ietf.jgss.GSSException; 041import org.ietf.jgss.GSSManager; 042import org.ietf.jgss.GSSName; 043import org.ietf.jgss.Oid; 044import org.slf4j.Logger; 045import org.slf4j.LoggerFactory; 046 047/** 048 * Thrift Http Servlet is used for performing Kerberos authentication if security is enabled and 049 * also used for setting the user specified in "doAs" parameter. 050 */ 051@InterfaceAudience.Private 052public class ThriftHttpServlet extends TServlet { 053 private static final long serialVersionUID = 1L; 054 private static final Logger LOG = LoggerFactory.getLogger(ThriftHttpServlet.class.getName()); 055 private final transient UserGroupInformation serviceUGI; 056 private final transient UserGroupInformation httpUGI; 057 private final transient HBaseServiceHandler handler; 058 private final boolean doAsEnabled; 059 private final boolean securityEnabled; 060 061 // HTTP Header related constants. 062 public static final String NEGOTIATE = "Negotiate"; 063 064 public ThriftHttpServlet(TProcessor processor, TProtocolFactory protocolFactory, 065 UserGroupInformation serviceUGI, UserGroupInformation httpUGI, 066 HBaseServiceHandler handler, boolean securityEnabled, boolean doAsEnabled) { 067 super(processor, protocolFactory); 068 this.serviceUGI = serviceUGI; 069 this.httpUGI = httpUGI; 070 this.handler = handler; 071 this.securityEnabled = securityEnabled; 072 this.doAsEnabled = doAsEnabled; 073 } 074 075 @Override 076 protected void doPost(HttpServletRequest request, HttpServletResponse response) 077 throws ServletException, IOException { 078 String effectiveUser = request.getRemoteUser(); 079 if (securityEnabled) { 080 /* 081 Check that the AUTHORIZATION header has any content. If it does not then return a 401 082 requesting AUTHORIZATION header to be sent. This is typical where the first request doesn't 083 send the AUTHORIZATION header initially. 084 */ 085 String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION); 086 if (authHeader == null || authHeader.isEmpty()) { 087 // Send a 401 to the client 088 response.addHeader(HttpHeaders.WWW_AUTHENTICATE, NEGOTIATE); 089 response.sendError(HttpServletResponse.SC_UNAUTHORIZED); 090 return; 091 } 092 093 try { 094 // As Thrift HTTP transport doesn't support SPNEGO yet (THRIFT-889), 095 // Kerberos authentication is being done at servlet level. 096 final RemoteUserIdentity identity = doKerberosAuth(request); 097 effectiveUser = identity.principal; 098 // It is standard for client applications expect this header. 099 // Please see http://tools.ietf.org/html/rfc4559 for more details. 100 response.addHeader(HttpHeaders.WWW_AUTHENTICATE, NEGOTIATE + " " + identity.outToken); 101 } catch (HttpAuthenticationException e) { 102 LOG.error("Kerberos Authentication failed", e); 103 // Send a 401 to the client 104 response.addHeader(HttpHeaders.WWW_AUTHENTICATE, NEGOTIATE); 105 response.sendError(HttpServletResponse.SC_UNAUTHORIZED, 106 "Authentication Error: " + e.getMessage()); 107 return; 108 } 109 } 110 111 if(effectiveUser == null) { 112 effectiveUser = serviceUGI.getShortUserName(); 113 } 114 115 String doAsUserFromQuery = request.getHeader("doAs"); 116 if (doAsUserFromQuery != null) { 117 if (!doAsEnabled) { 118 throw new ServletException("Support for proxyuser is not configured"); 119 } 120 // The authenticated remote user is attempting to perform 'doAs' proxy user. 121 UserGroupInformation remoteUser = UserGroupInformation.createRemoteUser(effectiveUser); 122 // create and attempt to authorize a proxy user (the client is attempting 123 // to do proxy user) 124 UserGroupInformation ugi = UserGroupInformation.createProxyUser(doAsUserFromQuery, 125 remoteUser); 126 // validate the proxy user authorization 127 try { 128 ProxyUsers.authorize(ugi, request.getRemoteAddr()); 129 } catch (AuthorizationException e) { 130 throw new ServletException(e); 131 } 132 effectiveUser = doAsUserFromQuery; 133 } 134 handler.setEffectiveUser(effectiveUser); 135 super.doPost(request, response); 136 } 137 138 /** 139 * Do the GSS-API kerberos authentication. 140 * We already have a logged in subject in the form of httpUGI, 141 * which GSS-API will extract information from. 142 */ 143 private RemoteUserIdentity doKerberosAuth(HttpServletRequest request) 144 throws HttpAuthenticationException { 145 HttpKerberosServerAction action = new HttpKerberosServerAction(request, httpUGI); 146 try { 147 String principal = httpUGI.doAs(action); 148 return new RemoteUserIdentity(principal, action.outToken); 149 } catch (Exception e) { 150 LOG.info("Failed to authenticate with {} kerberos principal", httpUGI.getUserName()); 151 throw new HttpAuthenticationException(e); 152 } 153 } 154 155 /** 156 * Basic "struct" class to hold the final base64-encoded, authenticated GSSAPI token 157 * for the user with the given principal talking to the Thrift server. 158 */ 159 private static class RemoteUserIdentity { 160 final String outToken; 161 final String principal; 162 163 RemoteUserIdentity(String principal, String outToken) { 164 this.principal = principal; 165 this.outToken = outToken; 166 } 167 } 168 169 private static class HttpKerberosServerAction implements PrivilegedExceptionAction<String> { 170 final HttpServletRequest request; 171 final UserGroupInformation httpUGI; 172 String outToken = null; 173 HttpKerberosServerAction(HttpServletRequest request, UserGroupInformation httpUGI) { 174 this.request = request; 175 this.httpUGI = httpUGI; 176 } 177 178 @Override 179 public String run() throws HttpAuthenticationException { 180 // Get own Kerberos credentials for accepting connection 181 GSSManager manager = GSSManager.getInstance(); 182 GSSContext gssContext = null; 183 String serverPrincipal = SecurityUtil.getPrincipalWithoutRealm(httpUGI.getUserName()); 184 try { 185 // This Oid for Kerberos GSS-API mechanism. 186 Oid kerberosMechOid = new Oid("1.2.840.113554.1.2.2"); 187 // Oid for SPNego GSS-API mechanism. 188 Oid spnegoMechOid = new Oid("1.3.6.1.5.5.2"); 189 // Oid for kerberos principal name 190 Oid krb5PrincipalOid = new Oid("1.2.840.113554.1.2.2.1"); 191 // GSS name for server 192 GSSName serverName = manager.createName(serverPrincipal, krb5PrincipalOid); 193 // GSS credentials for server 194 GSSCredential serverCreds = manager.createCredential(serverName, 195 GSSCredential.DEFAULT_LIFETIME, 196 new Oid[]{kerberosMechOid, spnegoMechOid}, 197 GSSCredential.ACCEPT_ONLY); 198 // Create a GSS context 199 gssContext = manager.createContext(serverCreds); 200 // Get service ticket from the authorization header 201 String serviceTicketBase64 = getAuthHeader(request); 202 byte[] inToken = Base64.getDecoder().decode(serviceTicketBase64); 203 byte[] res = gssContext.acceptSecContext(inToken, 0, inToken.length); 204 if(res != null) { 205 outToken = Base64.getEncoder().encodeToString(res).replace("\n", ""); 206 } 207 // Authenticate or deny based on its context completion 208 if (!gssContext.isEstablished()) { 209 throw new HttpAuthenticationException("Kerberos authentication failed: " + 210 "unable to establish context with the service ticket " + 211 "provided by the client."); 212 } 213 return SecurityUtil.getUserFromPrincipal(gssContext.getSrcName().toString()); 214 } catch (GSSException e) { 215 throw new HttpAuthenticationException("Kerberos authentication failed: ", e); 216 } finally { 217 if (gssContext != null) { 218 try { 219 gssContext.dispose(); 220 } catch (GSSException e) { 221 LOG.warn("Error while disposing GSS Context", e); 222 } 223 } 224 } 225 } 226 227 /** 228 * Returns the base64 encoded auth header payload 229 * 230 * @throws HttpAuthenticationException if a remote or network exception occurs 231 */ 232 private String getAuthHeader(HttpServletRequest request) 233 throws HttpAuthenticationException { 234 String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION); 235 // Each http request must have an Authorization header 236 if (authHeader == null || authHeader.isEmpty()) { 237 throw new HttpAuthenticationException("Authorization header received " + 238 "from the client is empty."); 239 } 240 String authHeaderBase64String; 241 int beginIndex = (NEGOTIATE + " ").length(); 242 authHeaderBase64String = authHeader.substring(beginIndex); 243 // Authorization header must have a payload 244 if (authHeaderBase64String.isEmpty()) { 245 throw new HttpAuthenticationException("Authorization header received " + 246 "from the client does not contain any data."); 247 } 248 return authHeaderBase64String; 249 } 250 } 251}