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>