You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sentry.apache.org by sp...@apache.org on 2018/12/04 22:03:52 UTC

[08/11] sentry git commit: SENTRY-2458: Separate Web UI and service from service-server to prevent circular dependencies (Brian Towels, reviewed by Na Li, Stephen Moist, kalyan kumar kalvagadda)

http://git-wip-us.apache.org/repos/asf/sentry/blob/ea7a33b7/sentry-service/sentry-service-server/src/main/webapp/css/sentry.css
----------------------------------------------------------------------
diff --git a/sentry-service/sentry-service-server/src/main/webapp/css/sentry.css b/sentry-service/sentry-service-server/src/main/webapp/css/sentry.css
deleted file mode 100644
index 69cba19..0000000
--- a/sentry-service/sentry-service-server/src/main/webapp/css/sentry.css
+++ /dev/null
@@ -1,52 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-html {
-  position: relative;
-  min-height: 100%;
-}
-
-body {
-  /* Margin bottom by footer height */
-  margin-bottom: 60px;
-  padding-top: 80px;
-}
-
-.navbar-collapse {margin-top:10px}
-
-.footer {
-  position: absolute;
-  bottom: 0;
-  width: 100%;
-  /* Set the fixed height of the footer here */
-  height: 60px;
-  background-color: #f5f5f5;
-}
-
-.container .text-muted {
-  margin: 20px 0;
-}
-
-.footer > .container {
-  padding-right: 15px;
-  padding-left: 15px;
-}
-
-code {
-  font-size: 80%;
-}

http://git-wip-us.apache.org/repos/asf/sentry/blob/ea7a33b7/sentry-service/sentry-service-server/src/main/webapp/sentry.png
----------------------------------------------------------------------
diff --git a/sentry-service/sentry-service-server/src/main/webapp/sentry.png b/sentry-service/sentry-service-server/src/main/webapp/sentry.png
deleted file mode 100644
index 67edd90..0000000
Binary files a/sentry-service/sentry-service-server/src/main/webapp/sentry.png and /dev/null differ

http://git-wip-us.apache.org/repos/asf/sentry/blob/ea7a33b7/sentry-service/sentry-service-server/src/test/java/org/apache/sentry/api/service/thrift/TestSentryWebServerWithoutSecurity.java
----------------------------------------------------------------------
diff --git a/sentry-service/sentry-service-server/src/test/java/org/apache/sentry/api/service/thrift/TestSentryWebServerWithoutSecurity.java b/sentry-service/sentry-service-server/src/test/java/org/apache/sentry/api/service/thrift/TestSentryWebServerWithoutSecurity.java
index 6e741e8..29adced 100644
--- a/sentry-service/sentry-service-server/src/test/java/org/apache/sentry/api/service/thrift/TestSentryWebServerWithoutSecurity.java
+++ b/sentry-service/sentry-service-server/src/test/java/org/apache/sentry/api/service/thrift/TestSentryWebServerWithoutSecurity.java
@@ -22,6 +22,7 @@ import java.net.URL;
 
 import org.apache.commons.io.IOUtils;
 import org.apache.sentry.service.thrift.SentryServiceIntegrationBase;
+import org.apache.sentry.service.web.ConfServlet;
 import org.junit.After;
 import org.junit.Assert;
 import org.junit.Before;

http://git-wip-us.apache.org/repos/asf/sentry/blob/ea7a33b7/sentry-service/sentry-service-web/pom.xml
----------------------------------------------------------------------
diff --git a/sentry-service/sentry-service-web/pom.xml b/sentry-service/sentry-service-web/pom.xml
new file mode 100644
index 0000000..53c6e3f
--- /dev/null
+++ b/sentry-service/sentry-service-web/pom.xml
@@ -0,0 +1,118 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one
+  ~ or more contributor license agreements.  See the NOTICE file
+  ~ distributed with this work for additional information
+  ~ regarding copyright ownership.  The ASF licenses this file
+  ~ to you under the Apache License, Version 2.0 (the
+  ~ "License"); you may not use this file except in compliance
+  ~ with the License.  You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  ~
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <parent>
+    <artifactId>sentry-service</artifactId>
+    <groupId>org.apache.sentry</groupId>
+    <version>2.2.0-SNAPSHOT</version>
+  </parent>
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>org.apache.sentry</groupId>
+  <artifactId>sentry-service-web</artifactId>
+
+  <build>
+    <sourceDirectory>${basedir}/src/main/java</sourceDirectory>
+    <testSourceDirectory>${basedir}/src/test/java</testSourceDirectory>
+    <resources>
+      <resource>
+        <directory>${basedir}/src/main</directory>
+        <includes>
+          <include>webapp/**</include>
+        </includes>
+        <filtering>true</filtering>
+      </resource>
+      <resource>
+        <directory>${basedir}/src/main/resources</directory>
+      </resource>
+    </resources>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-resources-plugin</artifactId>
+        <configuration>
+          <nonFilteredFileExtensions>
+            <nonFilteredFileExtension>eot</nonFilteredFileExtension>
+            <nonFilteredFileExtension>png</nonFilteredFileExtension>
+            <nonFilteredFileExtension>svg</nonFilteredFileExtension>
+            <nonFilteredFileExtension>ttf</nonFilteredFileExtension>
+            <nonFilteredFileExtension>woff</nonFilteredFileExtension>
+            <nonFilteredFileExtension>woff2</nonFilteredFileExtension>
+          </nonFilteredFileExtensions>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.projectlombok</groupId>
+      <artifactId>lombok</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.sentry</groupId>
+      <artifactId>sentry-core-common</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.sentry</groupId>
+      <artifactId>sentry-service-providers</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.sentry</groupId>
+      <artifactId>sentry-spi</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.eclipse.jetty</groupId>
+      <artifactId>jetty-server</artifactId>
+      <version>${jetty.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.eclipse.jetty</groupId>
+      <artifactId>jetty-servlet</artifactId>
+      <version>${jetty.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.hadoop</groupId>
+      <artifactId>hadoop-common</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-all</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.hadoop</groupId>
+      <artifactId>hadoop-minikdc</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+</project>

http://git-wip-us.apache.org/repos/asf/sentry/blob/ea7a33b7/sentry-service/sentry-service-web/src/main/java/org/apache/sentry/service/web/ConfServlet.java
----------------------------------------------------------------------
diff --git a/sentry-service/sentry-service-web/src/main/java/org/apache/sentry/service/web/ConfServlet.java b/sentry-service/sentry-service-web/src/main/java/org/apache/sentry/service/web/ConfServlet.java
new file mode 100644
index 0000000..b0fda0e
--- /dev/null
+++ b/sentry-service/sentry-service-web/src/main/java/org/apache/sentry/service/web/ConfServlet.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.sentry.service.web;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.hadoop.conf.Configuration;
+
+import static org.apache.commons.lang.StringEscapeUtils.escapeHtml;
+
+/**
+ * Servlet to print out all sentry configuration.
+ */
+public class ConfServlet extends HttpServlet {
+  public static final String CONF_CONTEXT_ATTRIBUTE = "sentry.conf";
+  public static final String FORMAT_JSON = "json";
+  public static final String FORMAT_XML = "xml";
+  public static final String FORMAT_PARAM = "format";
+  private static final long serialVersionUID = 1L;
+
+  @Override
+  public void doGet(HttpServletRequest request, HttpServletResponse response)
+      throws ServletException, IOException {
+    String format = request.getParameter(FORMAT_PARAM);
+    if (format == null) {
+      format = FORMAT_XML;
+    }
+
+    if (FORMAT_XML.equals(format)) {
+      response.setContentType("text/xml; charset=utf-8");
+    } else if (FORMAT_JSON.equals(format)) {
+      response.setContentType("application/json; charset=utf-8");
+    }
+
+    Configuration conf = (Configuration)getServletContext().getAttribute(
+        CONF_CONTEXT_ATTRIBUTE);
+    assert conf != null;
+
+    Writer out = response.getWriter();
+    if (FORMAT_JSON.equals(format)) {
+      Configuration.dumpConfiguration(conf, out);
+    } else if (FORMAT_XML.equals(format)) {
+      conf.writeXml(out);
+    } else {
+      response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Bad format: " + escapeHtml(format));
+    }
+    out.close();
+  }
+}

http://git-wip-us.apache.org/repos/asf/sentry/blob/ea7a33b7/sentry-service/sentry-service-web/src/main/java/org/apache/sentry/service/web/DefaultWebServicesProvider.java
----------------------------------------------------------------------
diff --git a/sentry-service/sentry-service-web/src/main/java/org/apache/sentry/service/web/DefaultWebServicesProvider.java b/sentry-service/sentry-service-web/src/main/java/org/apache/sentry/service/web/DefaultWebServicesProvider.java
new file mode 100644
index 0000000..9aafa29
--- /dev/null
+++ b/sentry-service/sentry-service-web/src/main/java/org/apache/sentry/service/web/DefaultWebServicesProvider.java
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.sentry.service.web;
+
+import com.google.common.base.Preconditions;
+import java.io.IOException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.servlet.DispatcherType;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.security.SecurityUtil;
+import org.apache.hadoop.security.UserGroupInformation;
+import org.apache.hadoop.security.authentication.server.AuthenticationFilter;
+import org.apache.sentry.server.provider.webservice.AttributeDesc;
+import org.apache.sentry.server.provider.webservice.FilterDesc;
+import org.apache.sentry.server.provider.webservice.WebServiceProvider;
+import org.apache.sentry.server.provider.webservice.WebServiceProviderFactory;
+import org.apache.sentry.server.provider.webservice.ServletDesc;
+import org.apache.sentry.service.common.ServiceConstants.ServerConfig;
+import org.eclipse.jetty.servlet.DefaultServlet;
+import org.eclipse.jetty.servlet.FilterHolder;
+import org.eclipse.jetty.servlet.ServletHolder;
+
+/**
+ * Provides the Web Interface functions for default Sentry services.
+ */
+@Slf4j
+public class DefaultWebServicesProvider implements WebServiceProvider,
+    WebServiceProviderFactory {
+
+  public static final String ID = "default";
+  private static final String STATIC_RESOURCE_DIR = "/webapp";
+
+  private Configuration config;
+
+  @Override
+  public List<ServletDesc> getServlets() {
+    List<ServletDesc> servlets = new ArrayList<>();
+    servlets.add(ServletDesc.of("/conf", new ServletHolder(ConfServlet.class)));
+    servlets.add(ServletDesc.of("/admin/logLevel", new ServletHolder(LogLevelServlet.class)));
+    if (config.getBoolean(ServerConfig.SENTRY_WEB_PUBSUB_SERVLET_ENABLED,
+        ServerConfig.SENTRY_WEB_PUBSUB_SERVLET_ENABLED_DEFAULT)) {
+      servlets.add(ServletDesc.of("/admin/publishMessage", new ServletHolder(PubSubServlet.class)));
+    }
+    if (config.getBoolean(ServerConfig.SENTRY_WEB_ADMIN_SERVLET_ENABLED,
+        ServerConfig.SENTRY_WEB_ADMIN_SERVLET_ENABLED_DEFAULT)) {
+      // Static files holder
+      ServletHolder staticHolder = new ServletHolder(new DefaultServlet());
+      staticHolder.setInitParameter("pathInfoOnly", "true");
+      URL url = this.getClass().getResource(STATIC_RESOURCE_DIR);
+      staticHolder.setInitParameter("resourceBase", url.toString());
+      servlets.add(ServletDesc.of("/*", staticHolder));
+    }
+
+    return servlets;
+  }
+
+  @Override
+  public List<AttributeDesc> getAttributes() {
+    return Arrays.asList(AttributeDesc.of(ConfServlet.CONF_CONTEXT_ATTRIBUTE, config));
+  }
+
+  @Override
+  public List<FilterDesc> getFilters() {
+    List<FilterDesc> filters = new ArrayList<>();
+    String authMethod = config.get(ServerConfig.SENTRY_WEB_SECURITY_TYPE);
+    if (!ServerConfig.SENTRY_WEB_SECURITY_TYPE_NONE.equalsIgnoreCase(authMethod)) {
+      /**
+       * SentryAuthFilter is a subclass of AuthenticationFilter and
+       * AuthenticationFilter tagged as private and unstable interface:
+       * While there are not guarantees that this interface will not change,
+       * it is fairly stable and used by other projects (ie - Oozie)
+       */
+      FilterHolder sentryAuthFilterHolder = new FilterHolder(SentryAuthFilter.class);
+      sentryAuthFilterHolder.setInitParameters(loadWebAuthenticationConf(config));
+      filters.add(FilterDesc.of("/*", sentryAuthFilterHolder, EnumSet.of(DispatcherType.REQUEST)));
+    }
+    return filters;
+  }
+
+  private static Map<String, String> loadWebAuthenticationConf(Configuration conf) {
+    Map<String, String> prop = new HashMap<String, String>();
+    prop.put(AuthenticationFilter.CONFIG_PREFIX, ServerConfig.SENTRY_WEB_SECURITY_PREFIX);
+    String allowUsers = conf.get(ServerConfig.SENTRY_WEB_SECURITY_ALLOW_CONNECT_USERS);
+    if (allowUsers == null || allowUsers.equals("")) {
+      allowUsers = conf.get(ServerConfig.ALLOW_CONNECT);
+      conf.set(ServerConfig.SENTRY_WEB_SECURITY_ALLOW_CONNECT_USERS, allowUsers);
+    }
+    validateConf(conf);
+    for (Map.Entry<String, String> entry : conf) {
+      String name = entry.getKey();
+      if (name.startsWith(ServerConfig.SENTRY_WEB_SECURITY_PREFIX)) {
+        String value = conf.get(name);
+        prop.put(name, value);
+      }
+    }
+    return prop;
+  }
+
+  private static void validateConf(Configuration conf) {
+    String authHandlerName = conf.get(ServerConfig.SENTRY_WEB_SECURITY_TYPE);
+    Preconditions.checkNotNull(authHandlerName, "Web authHandler should not be null.");
+    String allowUsers = conf.get(ServerConfig.SENTRY_WEB_SECURITY_ALLOW_CONNECT_USERS);
+    Preconditions.checkNotNull(allowUsers, "Allow connect user(s) should not be null.");
+    if (ServerConfig.SENTRY_WEB_SECURITY_TYPE_KERBEROS.equalsIgnoreCase(authHandlerName)) {
+      String principal = conf.get(ServerConfig.SENTRY_WEB_SECURITY_PRINCIPAL);
+      Preconditions.checkNotNull(principal, "Kerberos principal should not be null.");
+      Preconditions.checkArgument(principal.length() != 0, "Kerberos principal is not right.");
+      String keytabFile = conf.get(ServerConfig.SENTRY_WEB_SECURITY_KEYTAB);
+      Preconditions.checkNotNull(keytabFile, "Keytab File should not be null.");
+      Preconditions.checkArgument(keytabFile.length() != 0, "Keytab File is not right.");
+      try {
+        UserGroupInformation.setConfiguration(conf);
+        String hostPrincipal = SecurityUtil
+            .getServerPrincipal(principal, ServerConfig.RPC_ADDRESS_DEFAULT);
+        UserGroupInformation.loginUserFromKeytab(hostPrincipal, keytabFile);
+      } catch (IOException ex) {
+        throw new IllegalArgumentException("Can't use Kerberos authentication, principal ["
+            + principal + "] keytab [" + keytabFile + "]", ex);
+      }
+      LOGGER
+          .info("Using Kerberos authentication, principal [{}] keytab [{}]", principal, keytabFile);
+    }
+  }
+
+  @Override
+  public void init(Configuration config) {
+    this.config = config;
+  }
+
+  @Override
+  public WebServiceProvider create() {
+    return this;
+  }
+
+  @Override
+  public String getId() {
+    return ID;
+  }
+
+  @Override
+  public void close() {
+
+  }
+}

http://git-wip-us.apache.org/repos/asf/sentry/blob/ea7a33b7/sentry-service/sentry-service-web/src/main/java/org/apache/sentry/service/web/LogLevelServlet.java
----------------------------------------------------------------------
diff --git a/sentry-service/sentry-service-web/src/main/java/org/apache/sentry/service/web/LogLevelServlet.java b/sentry-service/sentry-service-web/src/main/java/org/apache/sentry/service/web/LogLevelServlet.java
new file mode 100644
index 0000000..a496779
--- /dev/null
+++ b/sentry-service/sentry-service-web/src/main/java/org/apache/sentry/service/web/LogLevelServlet.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package org.apache.sentry.service.web;
+
+import org.apache.log4j.Level;
+import org.apache.log4j.LogManager;
+import org.apache.log4j.Logger;
+
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.PrintWriter;
+
+import static org.apache.commons.lang.StringEscapeUtils.escapeHtml;
+
+public class LogLevelServlet extends HttpServlet {
+  private static final String LF = "\n";
+  private static final String BR = "<br />";
+  private static final String B_BR = "<b>%s</b><br />";
+  private static final String FORMS_HEAD =
+          "<h1>" + "Log Level" + "</h1>"
+                  + LF + BR + "<hr /><h3>Results</h3>"
+                  + LF + " Submitted Log Name: " + B_BR;
+  private static final String FORMS_CONTENT_GET =
+          LF + " Effective level: " + B_BR;
+  private static final String FORMS_CONTENT_SET =
+          LF + " Submitted Level: " + B_BR
+                  + LF + " Setting Level to %s" + BR
+                  + LF + " Effective level: " + B_BR;
+  private static final String FORMS_END =
+          LF + BR + "<hr /><h3>Get / Set</h3>"
+                  + LF + "<form>Log: <input type='text' size='50' name='log' /> "
+                  + "<input type='submit' value='Get Log Level' />" + "</form>"
+                  + LF + "<form>Log: <input type='text' size='50' name='log' /> "
+                  + "Level: <input type='text' name='level' /> "
+                  + "<input type='submit' value='Set Log Level' />" + "</form>";
+  private static final String FORMS_GET = FORMS_HEAD + FORMS_CONTENT_GET;
+  private static final String FORMS_SET = FORMS_HEAD + FORMS_CONTENT_SET;
+
+  /**
+   * Return parameter on servlet request for the given name
+   *
+   * @param request: Servlet request
+   * @param name: Name of parameter in servlet request
+   * @return Parameter in servlet request for the given name, return null if can't find parameter.
+   */
+  private String getParameter(ServletRequest request, String name) {
+    String s = request.getParameter(name);
+    if (s == null) {
+      return null;
+    }
+    s = s.trim();
+    return s.length() == 0 ? null : s;
+  }
+
+  /**
+   * Check the validity of the log level.
+   * @param level: The log level to be checked
+   * @return
+   *        true: The log level is valid
+   *        false: The log level is invalid
+   */
+  private boolean isLogLevelValid(String level) {
+    return level.equals(Level.toLevel(level).toString());
+  }
+
+  /**
+   * Parse the class name and log level in the http servlet request.
+   * If the request contains only class name, return the log level in the response message.
+   * If the request contains both class name and level, set the log level to the requested level
+   * and return the setting result in the response message.
+   */
+  @Override
+  public void doGet(HttpServletRequest request, HttpServletResponse response)
+          throws ServletException, IOException {
+    String logName = getParameter(request, "log");
+    String level = getParameter(request, "level");
+    response.setContentType("text/html;charset=utf-8");
+    response.setStatus(HttpServletResponse.SC_OK);
+    PrintWriter out = response.getWriter();
+
+    if (logName != null) {
+      Logger logInstance = LogManager.getLogger(logName);
+      if (level == null) {
+        out.write(String.format(FORMS_GET,
+                escapeHtml(logName),
+                logInstance.getEffectiveLevel().toString()));
+      } else if (isLogLevelValid(level)) {
+        logInstance.setLevel(Level.toLevel(level));
+        out.write(String.format(FORMS_SET,
+                escapeHtml(logName),
+                escapeHtml(level),
+                escapeHtml(level),
+                logInstance.getEffectiveLevel().toString()));
+      } else {
+        response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid log level: " + escapeHtml(level));
+        return;
+      }
+    }
+    out.write(FORMS_END);
+    out.close();
+    response.flushBuffer();
+  }
+}

http://git-wip-us.apache.org/repos/asf/sentry/blob/ea7a33b7/sentry-service/sentry-service-web/src/main/java/org/apache/sentry/service/web/PubSubServlet.java
----------------------------------------------------------------------
diff --git a/sentry-service/sentry-service-web/src/main/java/org/apache/sentry/service/web/PubSubServlet.java b/sentry-service/sentry-service-web/src/main/java/org/apache/sentry/service/web/PubSubServlet.java
new file mode 100644
index 0000000..c7e281b
--- /dev/null
+++ b/sentry-service/sentry-service-web/src/main/java/org/apache/sentry/service/web/PubSubServlet.java
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package org.apache.sentry.service.web;
+
+import org.apache.sentry.core.common.utils.PubSub;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.PrintWriter;
+
+import static org.apache.commons.lang.StringEscapeUtils.escapeHtml;
+
+/**
+ * This servlet facilitates sending {topic, message } tuples to Servlet components 
+ * subscribed to specific topics.
+ * <p>
+ * It uses publish-subscribe mechanism implemented by PubSub class.
+ * The form generated by this servlet consists of the following elements:
+ * <p>
+ * a) Topic: pull-down menu of existing topics, i.e. the topics registered with
+ * PubSub by calling PubSub.subscribe() API. This prevents entering invalid topic.
+ * <p>
+ * b) Message: text field for entering a message
+ * <p>
+ * c) Submit: button to submit (topic, message) tuple
+ * <p>
+ * d) Status: text area printing status of the request or help information.
+ */
+public class PubSubServlet extends HttpServlet {
+
+  private static final Logger LOGGER = LoggerFactory.getLogger(PubSubServlet.class);
+
+  private static final String FORM_GET =
+    "<!DOCTYPE html>" +
+    "<html>" +
+    "<body>" +
+    "<form>" +
+    "<br><br><b>Topic:</b><br><br>" +
+    "<select name='topic'/>%s</select>" +
+    "<br><br><b>Message:</b><br><br>" +
+    "<input type='text' size='50' name='message'/>" +
+    "<br><br>" +
+    "<input type='submit' value='Submit'/>" +
+    "</form>" +
+    "<br><br><b>Status:</b><br><br>" +
+    "<textarea rows='4' cols='50'>%s</textarea>" +
+    "</body>" +
+    "</html>";
+
+  /**
+   * Return parameter on servlet request for the given name
+   *
+   * @param request: Servlet request
+   * @param name: Name of parameter in servlet request
+   * @return Parameter in servlet request for the given name, return null if can't find parameter.
+   */
+  private static String getParameter(ServletRequest request, String name) {
+    String s = request.getParameter(name);
+    if (s == null) {
+      return null;
+    }
+    s = s.trim();
+    return s.isEmpty() ? null : s;
+  }
+
+  /**
+   * Parse the topic and message values and submit them via PubSub.submit() API.
+   * Reject request for unknown topic, i.e. topic no one is subscribed to.
+   */
+  @Override
+  public void doGet(HttpServletRequest request, HttpServletResponse response)
+          throws ServletException, IOException {
+    String topic = getParameter(request, "topic");
+    String message = getParameter(request, "message");
+    response.setContentType("text/html;charset=utf-8");
+    response.setStatus(HttpServletResponse.SC_OK);
+    PrintWriter out = response.getWriter();
+
+    String msg = "Topic is required, Message is optional.\nValid topics: " + PubSub.getInstance().getTopics();
+    if (topic != null) {
+      LOGGER.info("Submitting topic " + topic + ", message " + message);
+      try {
+        PubSub.getInstance().publish(PubSub.Topic.fromString(topic), message);
+        msg = "Submitted topic " + topic + ", message " + message;
+      } catch (Exception e) {
+        msg = "Failed to submit topic " + topic + ", message " + message + " - " + e.getMessage();
+        LOGGER.error(msg);
+        response.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
+        return;
+      }
+    }
+
+    StringBuilder topics = new StringBuilder();
+    for (PubSub.Topic t : PubSub.getInstance().getTopics()) {
+      topics.append("<option>").append(t.getName()).append("</option>");
+    }
+
+    String output = String.format(FORM_GET, topics.toString(), escapeHtml(msg));
+    if (LOGGER.isDebugEnabled()) {
+      LOGGER.debug("HTML Page: " + output);
+    }
+    out.write(output);
+    out.close();
+    response.flushBuffer();
+  }
+}

http://git-wip-us.apache.org/repos/asf/sentry/blob/ea7a33b7/sentry-service/sentry-service-web/src/main/java/org/apache/sentry/service/web/SentryAuthFilter.java
----------------------------------------------------------------------
diff --git a/sentry-service/sentry-service-web/src/main/java/org/apache/sentry/service/web/SentryAuthFilter.java b/sentry-service/sentry-service-web/src/main/java/org/apache/sentry/service/web/SentryAuthFilter.java
new file mode 100644
index 0000000..a6d75ad
--- /dev/null
+++ b/sentry-service/sentry-service-web/src/main/java/org/apache/sentry/service/web/SentryAuthFilter.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package org.apache.sentry.service.web;
+
+import java.io.IOException;
+import java.util.Enumeration;
+import java.util.Properties;
+import java.util.Set;
+
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.hadoop.security.authentication.server.AuthenticationFilter;
+import org.apache.hadoop.util.StringUtils;
+import org.apache.sentry.service.common.ServiceConstants.ServerConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.Sets;
+
+/**
+ * SentryAuthFilter is a subclass of AuthenticationFilter,
+ * add authorization: Only allowed users could connect the web server.
+ */
+public class SentryAuthFilter extends AuthenticationFilter {
+
+  private static final Logger LOG = LoggerFactory.getLogger(SentryAuthFilter.class);
+
+  public static final String ALLOW_WEB_CONNECT_USERS = ServerConfig.SENTRY_WEB_SECURITY_ALLOW_CONNECT_USERS;
+
+  private Set<String> allowUsers;
+
+  @Override
+  protected void doFilter(FilterChain filterChain, HttpServletRequest request,
+      HttpServletResponse response) throws IOException, ServletException {
+    String userName = request.getRemoteUser();
+    LOG.debug("Authenticating user: " + userName + " from request.");
+    if (!allowUsers.contains(userName)) {
+      response.sendError(HttpServletResponse.SC_FORBIDDEN,
+          "Unauthorized user status code: " + HttpServletResponse.SC_FORBIDDEN);
+      throw new ServletException(userName + " is unauthorized. status code: " + HttpServletResponse.SC_FORBIDDEN);
+    }
+    super.doFilter(filterChain, request, response);
+  }
+
+  /**
+   * Override <code>getConfiguration<code> to get <code>ALLOW_WEB_CONNECT_USERS<code>.
+   */
+  @Override
+  protected Properties getConfiguration(String configPrefix, FilterConfig filterConfig) throws ServletException {
+    Properties props = new Properties();
+    Enumeration<?> names = filterConfig.getInitParameterNames();
+    while (names.hasMoreElements()) {
+      String name = (String) names.nextElement();
+      if (name.startsWith(configPrefix)) {
+        String value = filterConfig.getInitParameter(name);
+        if (ALLOW_WEB_CONNECT_USERS.equals(name)) {
+          allowUsers = parseConnectUsersFromConf(value);
+        } else {
+          props.put(name.substring(configPrefix.length()), value);
+        }
+      }
+    }
+    return props;
+  }
+
+  private static Set<String> parseConnectUsersFromConf(String value) {
+    //Removed the logic to convert the allowed users to lower case, as user names need to be case sensitive
+    return Sets.newHashSet(StringUtils.getStrings(value));
+  }
+}

http://git-wip-us.apache.org/repos/asf/sentry/blob/ea7a33b7/sentry-service/sentry-service-web/src/main/java/org/apache/sentry/service/web/SentryWebServer.java
----------------------------------------------------------------------
diff --git a/sentry-service/sentry-service-web/src/main/java/org/apache/sentry/service/web/SentryWebServer.java b/sentry-service/sentry-service-web/src/main/java/org/apache/sentry/service/web/SentryWebServer.java
new file mode 100644
index 0000000..61af4e1
--- /dev/null
+++ b/sentry-service/sentry-service-web/src/main/java/org/apache/sentry/service/web/SentryWebServer.java
@@ -0,0 +1,180 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.sentry.service.web;
+
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+import com.google.common.collect.Sets;
+import java.util.EventListener;
+import java.util.List;
+import java.util.Set;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.sentry.server.provider.webservice.AttributeDesc;
+import org.apache.sentry.server.provider.webservice.FilterDesc;
+import org.apache.sentry.server.provider.webservice.WebServiceProvider;
+import org.apache.sentry.server.provider.webservice.WebServiceProviderFactory;
+import org.apache.sentry.server.provider.webservice.WebServiceSpi;
+import org.apache.sentry.server.provider.webservice.ServletDesc;
+import org.apache.sentry.service.common.ServiceConstants.ServerConfig;
+import org.apache.sentry.spi.ProviderManager;
+import org.eclipse.jetty.security.ConstraintMapping;
+import org.eclipse.jetty.security.ConstraintSecurityHandler;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.SecureRequestCustomizer;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.SslConnectionFactory;
+import org.eclipse.jetty.server.handler.ContextHandlerCollection;
+import org.eclipse.jetty.server.handler.DefaultHandler;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.util.security.Constraint;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+
+@Slf4j
+public class SentryWebServer {
+
+  private Server server;
+
+  public SentryWebServer(Configuration conf) {
+    server = new Server();
+
+    // Create a channel connector for "http/https" requests
+    ServerConnector connector;
+    int port = conf.getInt(ServerConfig.SENTRY_WEB_PORT, ServerConfig.SENTRY_WEB_PORT_DEFAULT);
+    if (conf.getBoolean(ServerConfig.SENTRY_WEB_USE_SSL, false)) {
+      SslContextFactory sslContextFactory = new SslContextFactory();
+      sslContextFactory.setKeyStorePath(conf.get(ServerConfig.SENTRY_WEB_SSL_KEYSTORE_PATH, ""));
+      sslContextFactory.setKeyStorePassword(
+          conf.get(ServerConfig.SENTRY_WEB_SSL_KEYSTORE_PASSWORD, ""));
+      // Exclude SSL blacklist protocols
+      sslContextFactory.setExcludeProtocols(ServerConfig.SENTRY_SSL_PROTOCOL_BLACKLIST_DEFAULT);
+      Set<String> moreExcludedSSLProtocols =
+          Sets.newHashSet(Splitter.on(",").trimResults().omitEmptyStrings()
+              .split(Strings.nullToEmpty(conf.get(ServerConfig.SENTRY_SSL_PROTOCOL_BLACKLIST))));
+      sslContextFactory.addExcludeProtocols(moreExcludedSSLProtocols.toArray(
+          new String[moreExcludedSSLProtocols.size()]));
+
+      HttpConfiguration httpConfiguration = new HttpConfiguration();
+      httpConfiguration.setSecurePort(port);
+      httpConfiguration.setSecureScheme("https");
+      httpConfiguration.addCustomizer(new SecureRequestCustomizer());
+
+      connector = new ServerConnector(
+          server,
+          new SslConnectionFactory(sslContextFactory, "http/1.1"),
+          new HttpConnectionFactory(httpConfiguration));
+
+      LOGGER.info("Now using SSL mode.");
+    } else {
+      connector = new ServerConnector(server, new HttpConnectionFactory());
+    }
+
+    connector.setPort(port);
+    server.setConnectors(new Connector[]{connector});
+
+    ServletContextHandler contextHandler = new ServletContextHandler();
+
+    // Load all of the Web Service Provider
+
+    // get the web service providers
+    List<WebServiceProviderFactory> serviceProviderFactories = ProviderManager.getInstance()
+        .load(WebServiceSpi.ID);
+
+    // initialize the factories
+    for (WebServiceProviderFactory providerFactory : serviceProviderFactories) {
+      providerFactory.init(conf);
+      WebServiceProvider provider = providerFactory.create();
+
+      // register its listeners
+      for (EventListener listener : provider.getListeners()) {
+        contextHandler.addEventListener(listener);
+      }
+
+      //register its attributes
+      for (AttributeDesc attributeEntry : provider.getAttributes()) {
+        contextHandler.getServletContext()
+            .setAttribute(attributeEntry.getName(), attributeEntry.getAttribute());
+      }
+
+      // register its servlets
+      for (ServletDesc servletEntry : provider.getServlets()) {
+        contextHandler
+            .addServlet(servletEntry.getServletHolder(), servletEntry.getPathSpec());
+      }
+
+      // register its filters
+      for (FilterDesc filterDesc : provider.getFilters()) {
+        contextHandler.addFilter(filterDesc.getFilterHolder(), filterDesc.getPathSpec(),
+            filterDesc.getDispatcherTypes());
+      }
+    }
+
+    ContextHandlerCollection contextHandlerCollection = new ContextHandlerCollection();
+    contextHandlerCollection.setHandlers(new Handler[]{contextHandler,
+        new DefaultHandler()});
+
+    server.setHandler(disableTraceMethod(contextHandlerCollection));
+  }
+
+  /**
+   * Disables the HTTP TRACE method request which leads to Cross-Site Tracking (XST) problems.
+   *
+   * To disable it, we need to wrap the Handler (which has the HTTP TRACE enabled) with a constraint
+   * that denies access to the HTTP TRACE method.
+   *
+   * @param handler The Handler which has the HTTP TRACE enabled.
+   * @return A new Handler wrapped with the HTTP TRACE constraint and the Handler passed as
+   * parameter.
+   */
+  private Handler disableTraceMethod(Handler handler) {
+    Constraint disableTraceConstraint = new Constraint();
+    disableTraceConstraint.setName("Disable TRACE");
+    disableTraceConstraint.setAuthenticate(true);
+
+    ConstraintMapping mapping = new ConstraintMapping();
+    mapping.setConstraint(disableTraceConstraint);
+    mapping.setMethod("TRACE");
+    mapping.setPathSpec("/");
+
+    ConstraintSecurityHandler constraintSecurityHandler = new ConstraintSecurityHandler();
+    constraintSecurityHandler.addConstraintMapping(mapping);
+    constraintSecurityHandler.setHandler(handler);
+
+    return constraintSecurityHandler;
+  }
+
+  public void start() throws Exception {
+    server.start();
+  }
+
+  public void stop() throws Exception {
+    server.stop();
+  }
+
+  public boolean isAlive() {
+    return server != null && server.isStarted();
+  }
+
+
+}

http://git-wip-us.apache.org/repos/asf/sentry/blob/ea7a33b7/sentry-service/sentry-service-web/src/main/resources/META-INF/services/org.apache.sentry.server.provider.webservice.WebServiceProviderFactory
----------------------------------------------------------------------
diff --git a/sentry-service/sentry-service-web/src/main/resources/META-INF/services/org.apache.sentry.server.provider.webservice.WebServiceProviderFactory b/sentry-service/sentry-service-web/src/main/resources/META-INF/services/org.apache.sentry.server.provider.webservice.WebServiceProviderFactory
new file mode 100644
index 0000000..8111e69
--- /dev/null
+++ b/sentry-service/sentry-service-web/src/main/resources/META-INF/services/org.apache.sentry.server.provider.webservice.WebServiceProviderFactory
@@ -0,0 +1,20 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+#
+
+org.apache.sentry.service.web.DefaultWebServicesProvider
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/sentry/blob/ea7a33b7/sentry-service/sentry-service-web/src/main/webapp/index.html
----------------------------------------------------------------------
diff --git a/sentry-service/sentry-service-web/src/main/webapp/index.html b/sentry-service/sentry-service-web/src/main/webapp/index.html
new file mode 100644
index 0000000..08df9d1
--- /dev/null
+++ b/sentry-service/sentry-service-web/src/main/webapp/index.html
@@ -0,0 +1,84 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one
+  ~ or more contributor license agreements.  See the NOTICE file
+  ~ distributed with this work for additional information
+  ~ regarding copyright ownership.  The ASF licenses this file
+  ~ to you under the Apache License, Version 2.0 (the
+  ~ "License"); you may not use this file except in compliance
+  ~ with the License.  You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  ~
+  -->
+<!DOCTYPE HTML>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <title>Sentry Service</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta name="description" content="">
+    <link href="/static/bootstrap/css/bootstrap-3.3.7.min.css" rel="stylesheet">
+    <link href="/static/bootstrap/css/bootstrap-theme-3.3.7.min.css" rel="stylesheet">
+    <link href="/static/materialdesign/css/materialdesignicons.min.css" rel="stylesheet">
+    <link href="/static/css/sentry.css" rel="stylesheet">
+  </head>
+
+  <body>
+    <nav class="navbar navbar-default navbar-fixed-top">
+      <div class="container">
+        <div class="navbar-header">
+          <a class="navbar-brand" href="#"><img src="/static/images/sentry.png" alt="Sentry Logo" width="48" height="36"/></a>
+        </div>
+        <div class="collapse navbar-collapse">
+          <ul class="nav navbar-nav">
+            <li class="active"><a href="#">Home</a></li>
+          </ul>
+        </div>
+      </div>
+    </nav>
+
+    <div class="container">
+      <div class="page-header"><h2>Sentry Service</h2></div>
+      <div class="panel panel-default">
+        <div class="panel-heading">
+          <h3 class="panel-title">Runtime Information</h3>
+        </div>
+        <div class="panel-body">
+            <div><a href="/conf"><span class="mdi mdi-settings"></span> Configuration</a></div>
+            <div><a href="/healthcheck"><span class="mdi mdi-heart-pulse"></span> Health Checks</a></div>
+            <div><a href="/metrics?pretty=true"><span class="mdi mdi-counter"></span> Metrics</a></div>
+            <div><a href="/threads"><span class="mdi mdi-source-fork"></span> Threads</a></div>
+        </div>
+      </div>
+      <div class="panel panel-default">
+        <div class="panel-heading">
+          <h3 class="panel-title">Runtime Modifications</h3>
+        </div>
+        <div class="panel-body">
+            <div><a href="/admin/logLevel"><span class="mdi mdi-card-bulleted-settings"></span> Log Level</a></div>
+            <div><a href="/admin/publishMessage"><span class="mdi mdi-message"></span> Publish Message</a></div>
+        </div>
+      </div>
+      <div class="panel panel-default">
+        <div class="panel-heading">
+          <h3 class="panel-title">Service Information</h3>
+        </div>
+        <div class="panel-body">
+            <div><a href="/admin/roles"><span class="mdi mdi-account-group"></span> Roles</a></div>
+        </div>
+      </div>
+    </div>
+
+    <footer class="footer">
+      <div class="container">
+        <p class="text-muted">${project.version}</p>
+      </div>
+    </footer>
+  </body>
+</html>