View Javadoc

1   /**
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  
19  package org.apache.hadoop.hbase.thrift;
20  
21  import java.io.IOException;
22  import java.security.PrivilegedExceptionAction;
23  
24  import javax.servlet.ServletException;
25  import javax.servlet.http.HttpServletRequest;
26  import javax.servlet.http.HttpServletResponse;
27  
28  import org.apache.commons.logging.Log;
29  import org.apache.commons.logging.LogFactory;
30  import org.apache.commons.net.util.Base64;
31  import org.apache.hadoop.conf.Configuration;
32  import org.apache.hadoop.hbase.classification.InterfaceAudience;
33  import org.apache.hadoop.hbase.security.SecurityUtil;
34  import org.apache.hadoop.security.UserGroupInformation;
35  import org.apache.hadoop.security.authorize.AuthorizationException;
36  import org.apache.hadoop.security.authorize.ProxyUsers;
37  import org.apache.thrift.TProcessor;
38  import org.apache.thrift.protocol.TProtocolFactory;
39  import org.apache.thrift.server.TServlet;
40  import org.ietf.jgss.GSSContext;
41  import org.ietf.jgss.GSSCredential;
42  import org.ietf.jgss.GSSException;
43  import org.ietf.jgss.GSSManager;
44  import org.ietf.jgss.GSSName;
45  import org.ietf.jgss.Oid;
46  
47  /**
48   * Thrift Http Servlet is used for performing Kerberos authentication if security is enabled and
49   * also used for setting the user specified in "doAs" parameter.
50   */
51  @InterfaceAudience.Private
52  public class ThriftHttpServlet extends TServlet {
53    private static final long serialVersionUID = 1L;
54    public static final Log LOG = LogFactory.getLog(ThriftHttpServlet.class.getName());
55    private transient final UserGroupInformation realUser;
56    private transient final Configuration conf;
57    private final boolean securityEnabled;
58    private final boolean doAsEnabled;
59    private transient ThriftServerRunner.HBaseHandler hbaseHandler;
60  
61    public ThriftHttpServlet(TProcessor processor, TProtocolFactory protocolFactory,
62        UserGroupInformation realUser, Configuration conf, ThriftServerRunner.HBaseHandler
63        hbaseHandler, boolean securityEnabled, boolean doAsEnabled) {
64      super(processor, protocolFactory);
65      this.realUser = realUser;
66      this.conf = conf;
67      this.hbaseHandler = hbaseHandler;
68      this.securityEnabled = securityEnabled;
69      this.doAsEnabled = doAsEnabled;
70    }
71  
72    @Override
73    protected void doPost(HttpServletRequest request, HttpServletResponse response)
74        throws ServletException, IOException {
75      String effectiveUser = realUser.getShortUserName();
76      if (securityEnabled) {
77        try {
78          // As Thrift HTTP transport doesn't support SPNEGO yet (THRIFT-889),
79          // Kerberos authentication is being done at servlet level.
80          effectiveUser = doKerberosAuth(request);
81        } catch (HttpAuthenticationException e) {
82          LOG.error("Kerberos Authentication failed", e);
83          // Send a 401 to the client
84          response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
85          response.getWriter().println("Authentication Error: " + e.getMessage());
86        }
87      }
88      String doAsUserFromQuery = request.getHeader("doAs");
89      if (doAsUserFromQuery != null) {
90        if (!doAsEnabled) {
91          throw new ServletException("Support for proxyuser is not configured");
92        }
93        // create and attempt to authorize a proxy user (the client is attempting
94        // to do proxy user)
95        UserGroupInformation ugi = UserGroupInformation.createProxyUser(doAsUserFromQuery, realUser);
96        // validate the proxy user authorization
97        try {
98          ProxyUsers.authorize(ugi, request.getRemoteAddr(), conf);
99        } catch (AuthorizationException e) {
100         throw new ServletException(e.getMessage());
101       }
102       effectiveUser = doAsUserFromQuery;
103     }
104     hbaseHandler.setEffectiveUser(effectiveUser);
105     super.doPost(request, response);
106   }
107 
108   /**
109    * Do the GSS-API kerberos authentication.
110    * We already have a logged in subject in the form of serviceUGI,
111    * which GSS-API will extract information from.
112    */
113   private String doKerberosAuth(HttpServletRequest request)
114       throws HttpAuthenticationException {
115     try {
116       return realUser.doAs(new HttpKerberosServerAction(request, realUser));
117     } catch (Exception e) {
118       LOG.error("Failed to perform authentication");
119       throw new HttpAuthenticationException(e);
120     }
121   }
122 
123 
124   private static class HttpKerberosServerAction implements PrivilegedExceptionAction<String> {
125     HttpServletRequest request;
126     UserGroupInformation serviceUGI;
127     HttpKerberosServerAction(HttpServletRequest request, UserGroupInformation serviceUGI) {
128       this.request = request;
129       this.serviceUGI = serviceUGI;
130     }
131 
132     @Override
133     public String run() throws HttpAuthenticationException {
134       // Get own Kerberos credentials for accepting connection
135       GSSManager manager = GSSManager.getInstance();
136       GSSContext gssContext = null;
137       String serverPrincipal = SecurityUtil.getPrincipalWithoutRealm(serviceUGI.getUserName());
138       try {
139         // This Oid for Kerberos GSS-API mechanism.
140         Oid kerberosMechOid = new Oid("1.2.840.113554.1.2.2");
141         // Oid for SPNego GSS-API mechanism.
142         Oid spnegoMechOid = new Oid("1.3.6.1.5.5.2");
143         // Oid for kerberos principal name
144         Oid krb5PrincipalOid = new Oid("1.2.840.113554.1.2.2.1");
145         // GSS name for server
146         GSSName serverName = manager.createName(serverPrincipal, krb5PrincipalOid);
147         // GSS credentials for server
148         GSSCredential serverCreds = manager.createCredential(serverName,
149             GSSCredential.DEFAULT_LIFETIME,
150             new Oid[]{kerberosMechOid, spnegoMechOid},
151             GSSCredential.ACCEPT_ONLY);
152         // Create a GSS context
153         gssContext = manager.createContext(serverCreds);
154         // Get service ticket from the authorization header
155          String serviceTicketBase64 = getAuthHeader(request);
156          byte[] inToken = Base64.decodeBase64(serviceTicketBase64.getBytes());
157          gssContext.acceptSecContext(inToken, 0, inToken.length);
158          // Authenticate or deny based on its context completion
159          if (!gssContext.isEstablished()) {
160           throw new HttpAuthenticationException("Kerberos authentication failed: " +
161               "unable to establish context with the service ticket " +
162               "provided by the client.");
163          }
164          return SecurityUtil.getUserFromPrincipal(gssContext.getSrcName().toString());
165       } catch (GSSException e) {
166         throw new HttpAuthenticationException("Kerberos authentication failed: ", e);
167       } finally {
168         if (gssContext != null) {
169           try {
170             gssContext.dispose();
171           } catch (GSSException e) {
172             LOG.warn("Error while disposing GSS Context", e);
173           }
174         }
175       }
176     }
177 
178     /**
179      * Returns the base64 encoded auth header payload
180      *
181      * @throws HttpAuthenticationException if a remote or network exception occurs
182      */
183     private String getAuthHeader(HttpServletRequest request)
184         throws HttpAuthenticationException {
185       String authHeader = request.getHeader("Authorization");
186       // Each http request must have an Authorization header
187       if (authHeader == null || authHeader.isEmpty()) {
188         throw new HttpAuthenticationException("Authorization header received " +
189             "from the client is empty.");
190       }
191       String authHeaderBase64String;
192       int beginIndex = ("Negotiate ").length();
193       authHeaderBase64String = authHeader.substring(beginIndex);
194       // Authorization header must have a payload
195       if (authHeaderBase64String == null || authHeaderBase64String.isEmpty()) {
196         throw new HttpAuthenticationException("Authorization header received " +
197             "from the client does not contain any data.");
198       }
199       return authHeaderBase64String;
200     }
201   }
202 }