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}