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}