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