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.http;
019
020import java.io.IOException;
021import java.net.URI;
022import javax.servlet.ServletContext;
023import javax.servlet.http.HttpServlet;
024import javax.servlet.http.HttpServletRequest;
025import org.apache.commons.lang3.StringUtils;
026import org.apache.hadoop.conf.Configuration;
027import org.apache.hadoop.fs.CommonConfigurationKeys;
028import org.apache.hadoop.hbase.HBaseConfiguration;
029import org.apache.hadoop.hbase.io.crypto.tls.X509Util;
030import org.apache.hadoop.security.authorize.AccessControlList;
031import org.apache.yetus.audience.InterfaceAudience;
032
033import org.apache.hbase.thirdparty.com.google.common.net.HostAndPort;
034import org.apache.hbase.thirdparty.org.eclipse.jetty.ee8.servlet.ServletHolder;
035
036/**
037 * Create a Jetty embedded server to answer http requests. The primary goal is to serve up status
038 * information for the server. There are three contexts: "/stacks/" -> points to stack trace
039 * "/static/" -> points to common static files (src/hbase-webapps/static) "/" -> the jsp
040 * server code from (src/hbase-webapps/<name>)
041 */
042@InterfaceAudience.Private
043public class InfoServer {
044  private static final String HBASE_APP_DIR = "hbase-webapps";
045  private final org.apache.hadoop.hbase.http.HttpServer httpServer;
046
047  private static final String HADOOP_WEB_TLS_CONFIG_PREFIX = "ssl.server.";
048  private static final String HBASE_WEB_TLS_CONFIG_PREFIX = "hbase.ui.ssl.";
049  // Role-scoped prefix for single-EKU certificate support. When set, takes precedence over both
050  // HBASE_WEB_TLS_CONFIG_PREFIX and HADOOP_WEB_TLS_CONFIG_PREFIX. The UI process only ever plays
051  // the TLS-server role, so no parallel .client. prefix is defined.
052  private static final String HBASE_WEB_TLS_SERVER_CONFIG_PREFIX = "hbase.ui.ssl.server.";
053  private static final String TRUSTSTORE_LOCATION = "truststore.location";
054  /**
055   * Config key controlling whether the UI's TLS connector requests or requires a client
056   * certificate. Valid values: {@code NONE} (default), {@code WANT}, {@code NEED}.
057   */
058  static final String HBASE_UI_SSL_CLIENT_AUTH_MODE =
059    HBASE_WEB_TLS_SERVER_CONFIG_PREFIX + "client.auth.mode";
060
061  /**
062   * Create a status server on the given port. The jsp scripts are taken from
063   * src/hbase-webapps/<code>name</code>.
064   * @param name        The name of the server
065   * @param bindAddress address to bind to
066   * @param port        The port to use on the server
067   * @param findPort    whether the server should start at the given port and increment by 1 until
068   *                    it finds a free port.
069   * @param c           the {@link Configuration} to build the server
070   * @throws IOException if getting one of the password fails or the server cannot be created
071   */
072  public InfoServer(String name, String bindAddress, int port, boolean findPort,
073    final Configuration c) throws IOException {
074    HttpConfig httpConfig = new HttpConfig(c);
075    HttpServer.Builder builder = new org.apache.hadoop.hbase.http.HttpServer.Builder();
076
077    builder.setName(name)
078      .addEndpoint(URI
079        .create(httpConfig.getSchemePrefix() + HostAndPort.fromParts(bindAddress, port).toString()))
080      .setAppDir(HBASE_APP_DIR).setFindPort(findPort).setConf(c);
081    String logDir = System.getProperty("hbase.log.dir");
082    if (logDir != null) {
083      builder.setLogDir(logDir);
084    }
085    if (httpConfig.isSecure()) {
086      // We are using the Hadoop HTTP server config properties.
087      // This makes it easy to keep in sync with Hadoop's UI servers, but hard to set this
088      // separately for HBase.
089      String trustStoreLocation = getTLSProperty(c, TRUSTSTORE_LOCATION);
090
091      builder.keyPassword(getTLSPassword(c, "keystore.keypassword"))
092        .keyStore(getTLSProperty(c, "keystore.location"), getTLSPassword(c, "keystore.password"),
093          getTLSProperty(c, "keystore.type", "jks"))
094        .trustStore(trustStoreLocation, getTLSPassword(c, "truststore.password"),
095          getTLSProperty(c, "truststore.type", "jks"))
096        // The ssl.server.*.protocols properties do not exist in Hadoop at the time of writing.
097        .setIncludeProtocols(getTLSProperty(c, "include.protocols"))
098        .setExcludeProtocols(getTLSProperty(c, "exclude.protocols"))
099        .setIncludeCiphers(getTLSProperty(c, "include.cipher.list"))
100        .setExcludeCiphers(getTLSProperty(c, "exclude.cipher.list"));
101
102      // Activate mutual TLS if configured. Default is NONE, which preserves today's behavior of
103      // never requesting a client certificate on the UI connector (leaving any configured
104      // truststore inert for peer verification). Set hbase.ui.ssl.server.client.auth.mode to
105      // WANT or NEED to opt in. The client.auth.mode key is looked up directly on
106      // HBASE_WEB_TLS_SERVER_CONFIG_PREFIX; there is no legacy or Hadoop-prefixed fallback.
107      // getTrimmed + defaultIfBlank so that an empty or whitespace-only <value></value> in
108      // hbase-site.xml behaves like an absent key. A bare fromPropertyValue("") would return NEED.
109      X509Util.ClientAuth clientAuth =
110        X509Util.ClientAuth.fromPropertyValue(StringUtils.defaultIfBlank(
111          c.getTrimmed(HBASE_UI_SSL_CLIENT_AUTH_MODE), X509Util.ClientAuth.NONE.name()));
112      X509Util.validateClientAuthTrustStore(clientAuth, trustStoreLocation,
113        HBASE_UI_SSL_CLIENT_AUTH_MODE, HBASE_WEB_TLS_SERVER_CONFIG_PREFIX + TRUSTSTORE_LOCATION,
114        HBASE_WEB_TLS_CONFIG_PREFIX + TRUSTSTORE_LOCATION,
115        HADOOP_WEB_TLS_CONFIG_PREFIX + TRUSTSTORE_LOCATION);
116      builder.needsClientAuth(clientAuth == X509Util.ClientAuth.NEED)
117        .wantsClientAuth(clientAuth == X509Util.ClientAuth.WANT);
118    }
119
120    final String httpAuthType = c.get(HttpServer.HTTP_UI_AUTHENTICATION, "").toLowerCase();
121    // Enable SPNEGO authentication
122    if ("kerberos".equals(httpAuthType)) {
123      builder.setUsernameConfKey(HttpServer.HTTP_SPNEGO_AUTHENTICATION_PRINCIPAL_KEY)
124        .setKeytabConfKey(HttpServer.HTTP_SPNEGO_AUTHENTICATION_KEYTAB_KEY)
125        .setKerberosNameRulesKey(HttpServer.HTTP_SPNEGO_AUTHENTICATION_KRB_NAME_KEY)
126        .setSignatureSecretFileKey(HttpServer.HTTP_AUTHENTICATION_SIGNATURE_SECRET_FILE_KEY)
127        .setSecurityEnabled(true);
128    }
129
130    // Set an admin ACL on sensitive webUI endpoints (works only if SPNEGO or LDAP is enabled)
131    if ("ldap".equals(httpAuthType) || "kerberos".equals(httpAuthType)) {
132      AccessControlList acl = buildAdminAcl(c);
133      builder.setACL(acl);
134    }
135
136    this.httpServer = builder.build();
137  }
138
139  /**
140   * Resolves a TLS-related password with a 3-tier fallback: the role-scoped
141   * {@code hbase.ui.ssl.server.<postfix>} key is checked first, then the unscoped
142   * {@code hbase.ui.ssl.<postfix>} key, and finally Hadoop's {@code ssl.server.<postfix>}.
143   * Package-private for direct testing.
144   */
145  static String getTLSPassword(Configuration c, String postfix) throws IOException {
146    return HBaseConfiguration.getPassword(c, HBASE_WEB_TLS_SERVER_CONFIG_PREFIX + postfix,
147      HBaseConfiguration.getPassword(c, HBASE_WEB_TLS_CONFIG_PREFIX + postfix,
148        HBaseConfiguration.getPassword(c, HADOOP_WEB_TLS_CONFIG_PREFIX + postfix, null)));
149  }
150
151  static String getTLSProperty(Configuration c, String postfix) {
152    return getTLSProperty(c, postfix, null);
153  }
154
155  /**
156   * Resolves a TLS-related property with a 3-tier fallback: role-scoped
157   * {@code hbase.ui.ssl.server.<postfix>} → {@code hbase.ui.ssl.<postfix>} →
158   * {@code ssl.server.<postfix>} → {@code defaultValue}. Package-private for direct testing.
159   */
160  static String getTLSProperty(Configuration c, String postfix, String defaultValue) {
161    return c.get(HBASE_WEB_TLS_SERVER_CONFIG_PREFIX + postfix,
162      c.get(HBASE_WEB_TLS_CONFIG_PREFIX + postfix,
163        c.get(HADOOP_WEB_TLS_CONFIG_PREFIX + postfix, defaultValue)));
164  }
165
166  /**
167   * Builds an ACL that will restrict the users who can issue commands to endpoints on the UI which
168   * are meant only for administrators.
169   */
170  static AccessControlList buildAdminAcl(Configuration conf) {
171    // Initialize admin users based on whether http ui auth is set to ldap or kerberos
172    String httpAuthType = conf.get(HttpServer.HTTP_UI_AUTHENTICATION, "").toLowerCase();
173    final String adminUsers = getAdminUsers(conf, httpAuthType);
174    final String adminGroups =
175      conf.get(HttpServer.HTTP_SPNEGO_AUTHENTICATION_ADMIN_GROUPS_KEY, null);
176    if (adminUsers == null && adminGroups == null) {
177      // Backwards compatibility - if the user doesn't have anything set, allow all users in.
178      return new AccessControlList("*", null);
179    }
180    return new AccessControlList(adminUsers, adminGroups);
181  }
182
183  private static String getAdminUsers(Configuration conf, String httpAuthType) {
184    if ("kerberos".equals(httpAuthType)) {
185      return conf.get(HttpServer.HTTP_SPNEGO_AUTHENTICATION_ADMIN_USERS_KEY, null);
186    } else if ("ldap".equals(httpAuthType)) {
187      return conf.get(HttpServer.HTTP_LDAP_AUTHENTICATION_ADMIN_USERS_KEY, null);
188    }
189    // If the auth type is not kerberos or ldap, return null
190    return null;
191  }
192
193  /**
194   * Explicitly invoke {@link #addPrivilegedServlet(String, String, Class)} or
195   * {@link #addUnprivilegedServlet(String, String, Class)} instead of this method. This method will
196   * add a servlet which any authenticated user can access.
197   * @deprecated Use {@link #addUnprivilegedServlet(String, String, Class)} or
198   *             {@link #addPrivilegedServlet(String, String, Class)} instead of this method which
199   *             does not state outwardly what kind of authz rules will be applied to this servlet.
200   */
201  @Deprecated
202  public void addServlet(String name, String pathSpec, Class<? extends HttpServlet> clazz) {
203    addUnprivilegedServlet(name, pathSpec, clazz);
204  }
205
206  /**
207   * Adds a servlet in the server that any user can access.
208   * @see HttpServer#addUnprivilegedServlet(String, String, Class)
209   */
210  public void addUnprivilegedServlet(String name, String pathSpec,
211    Class<? extends HttpServlet> clazz) {
212    this.httpServer.addUnprivilegedServlet(name, pathSpec, clazz);
213  }
214
215  /**
216   * Adds a servlet in the server that any user can access.
217   * @see HttpServer#addUnprivilegedServlet(String, ServletHolder)
218   */
219  public void addUnprivilegedServlet(String name, String pathSpec, ServletHolder holder) {
220    if (name != null) {
221      holder.setName(name);
222    }
223    this.httpServer.addUnprivilegedServlet(pathSpec, holder);
224  }
225
226  /**
227   * Adds a servlet in the server that any user can access.
228   * @see HttpServer#addPrivilegedServlet(String, String, Class)
229   */
230  public void addPrivilegedServlet(String name, String pathSpec,
231    Class<? extends HttpServlet> clazz) {
232    this.httpServer.addPrivilegedServlet(name, pathSpec, clazz);
233  }
234
235  public void setAttribute(String name, Object value) {
236    this.httpServer.setAttribute(name, value);
237  }
238
239  public void start() throws IOException {
240    this.httpServer.start();
241  }
242
243  /**
244   * @return the port of the info server
245   * @deprecated Since 0.99.0
246   */
247  @Deprecated
248  public int getPort() {
249    return this.httpServer.getPort();
250  }
251
252  public void stop() throws Exception {
253    this.httpServer.stop();
254  }
255
256  /**
257   * Returns true if and only if UI authentication (spnego) is enabled, UI authorization is enabled,
258   * and the requesting user is defined as an administrator. If the UI is set to readonly, this
259   * method always returns false.
260   */
261  public static boolean canUserModifyUI(HttpServletRequest req, ServletContext ctx,
262    Configuration conf) {
263    if (conf.getBoolean("hbase.master.ui.readonly", false)) {
264      return false;
265    }
266    String remoteUser = req.getRemoteUser();
267    if (
268      "kerberos".equalsIgnoreCase(conf.get(HttpServer.HTTP_UI_AUTHENTICATION))
269        && conf.getBoolean(CommonConfigurationKeys.HADOOP_SECURITY_AUTHORIZATION, false)
270        && remoteUser != null
271    ) {
272      return HttpServer.userHasAdministratorAccess(ctx, remoteUser);
273    }
274    return false;
275  }
276}