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