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}