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.yetus.audience.InterfaceAudience; 034import org.ietf.jgss.GSSContext; 035import org.ietf.jgss.GSSCredential; 036import org.ietf.jgss.GSSException; 037import org.ietf.jgss.GSSManager; 038import org.ietf.jgss.GSSName; 039import org.ietf.jgss.Oid; 040import org.slf4j.Logger; 041import org.slf4j.LoggerFactory; 042 043import org.apache.hbase.thirdparty.org.apache.thrift.TProcessor; 044import org.apache.hbase.thirdparty.org.apache.thrift.protocol.TProtocolFactory; 045import org.apache.hbase.thirdparty.org.apache.thrift.server.TServlet; 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, HBaseServiceHandler handler, 066 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 = getDoasFromHeader(request); 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 = 125 UserGroupInformation.createProxyUser(doAsUserFromQuery, 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. We already have a logged in subject in the form of 140 * httpUGI, which GSS-API will extract information from. 141 */ 142 private RemoteUserIdentity doKerberosAuth(HttpServletRequest request) 143 throws HttpAuthenticationException { 144 HttpKerberosServerAction action = new HttpKerberosServerAction(request, httpUGI); 145 try { 146 String principal = httpUGI.doAs(action); 147 return new RemoteUserIdentity(principal, action.outToken); 148 } catch (Exception e) { 149 LOG.info("Failed to authenticate with {} kerberos principal", httpUGI.getUserName()); 150 throw new HttpAuthenticationException(e); 151 } 152 } 153 154 /** 155 * Basic "struct" class to hold the final base64-encoded, authenticated GSSAPI token for the user 156 * with the given principal talking to the Thrift server. 157 */ 158 private static class RemoteUserIdentity { 159 final String outToken; 160 final String principal; 161 162 RemoteUserIdentity(String principal, String outToken) { 163 this.principal = principal; 164 this.outToken = outToken; 165 } 166 } 167 168 private static class HttpKerberosServerAction implements PrivilegedExceptionAction<String> { 169 final HttpServletRequest request; 170 final UserGroupInformation httpUGI; 171 String outToken = null; 172 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 = 195 manager.createCredential(serverName, GSSCredential.DEFAULT_LIFETIME, 196 new Oid[] { kerberosMechOid, spnegoMechOid }, GSSCredential.ACCEPT_ONLY); 197 // Create a GSS context 198 gssContext = manager.createContext(serverCreds); 199 // Get service ticket from the authorization header 200 String serviceTicketBase64 = getAuthHeader(request); 201 byte[] inToken = Base64.getDecoder().decode(serviceTicketBase64); 202 byte[] res = gssContext.acceptSecContext(inToken, 0, inToken.length); 203 if (res != null) { 204 outToken = Base64.getEncoder().encodeToString(res).replace("\n", ""); 205 } 206 // Authenticate or deny based on its context completion 207 if (!gssContext.isEstablished()) { 208 throw new HttpAuthenticationException("Kerberos authentication failed: " 209 + "unable to establish context with the service ticket " + "provided by the client."); 210 } 211 return SecurityUtil.getUserFromPrincipal(gssContext.getSrcName().toString()); 212 } catch (GSSException e) { 213 throw new HttpAuthenticationException("Kerberos authentication failed: ", e); 214 } finally { 215 if (gssContext != null) { 216 try { 217 gssContext.dispose(); 218 } catch (GSSException e) { 219 LOG.warn("Error while disposing GSS Context", e); 220 } 221 } 222 } 223 } 224 225 /** 226 * Returns the base64 encoded auth header payload 227 * @throws HttpAuthenticationException if a remote or network exception occurs 228 */ 229 private String getAuthHeader(HttpServletRequest request) throws HttpAuthenticationException { 230 String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION); 231 // Each http request must have an Authorization header 232 if (authHeader == null || authHeader.isEmpty()) { 233 throw new HttpAuthenticationException( 234 "Authorization header received " + "from the client is empty."); 235 } 236 String authHeaderBase64String; 237 int beginIndex = (NEGOTIATE + " ").length(); 238 authHeaderBase64String = authHeader.substring(beginIndex); 239 // Authorization header must have a payload 240 if (authHeaderBase64String.isEmpty()) { 241 throw new HttpAuthenticationException( 242 "Authorization header received " + "from the client does not contain any data."); 243 } 244 return authHeaderBase64String; 245 } 246 } 247}