DRILL-7351: Added tokens to Web forms to prevent CSRF attacks
diff --git a/drill-yarn/src/main/java/org/apache/drill/yarn/appMaster/DrillApplicationMaster.java b/drill-yarn/src/main/java/org/apache/drill/yarn/appMaster/DrillApplicationMaster.java
index c0db9a1..e32a531 100644
--- a/drill-yarn/src/main/java/org/apache/drill/yarn/appMaster/DrillApplicationMaster.java
+++ b/drill-yarn/src/main/java/org/apache/drill/yarn/appMaster/DrillApplicationMaster.java
@@ -19,6 +19,8 @@
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
+import org.apache.drill.common.util.GuavaPatcher;
+import org.apache.drill.common.util.ProtobufPatcher;
import org.apache.drill.yarn.appMaster.ControllerFactory.ControllerFactoryException;
import org.apache.drill.yarn.appMaster.http.WebServer;
import org.apache.drill.yarn.core.DoyConfigException;
@@ -44,6 +46,22 @@
*/
public class DrillApplicationMaster {
+
+ static {
+ /*
+ * Drill-on-YARN uses Hadoop dependencies that use older version of protobuf,
+ * and override some methods that became final in recent protobuf versions.
+ * This code removes these final modifiers.
+ */
+ ProtobufPatcher.patch();
+ /*
+ * HBase client uses older version of Guava's Stopwatch API,
+ * while Drill ships with 18.x which has changes the scope of
+ * these API to 'package', this code make them accessible.
+ */
+ GuavaPatcher.patch();
+ }
+
private static final Log LOG = LogFactory
.getLog(DrillApplicationMaster.class);
diff --git a/drill-yarn/src/main/java/org/apache/drill/yarn/appMaster/http/CsrfTokenInjectFilter.java b/drill-yarn/src/main/java/org/apache/drill/yarn/appMaster/http/CsrfTokenInjectFilter.java
new file mode 100644
index 0000000..1f282d8
--- /dev/null
+++ b/drill-yarn/src/main/java/org/apache/drill/yarn/appMaster/http/CsrfTokenInjectFilter.java
@@ -0,0 +1,60 @@
+/*
+ * 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.drill.yarn.appMaster.http;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+import javax.ws.rs.HttpMethod;
+import java.io.IOException;
+
+/**
+ * Generates and adds a CSRF token to a HTTP session. It is used to prevent CSRF attacks.
+ */
+public class CsrfTokenInjectFilter implements Filter {
+
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException {
+ }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+ HttpServletRequest httpRequest = (HttpServletRequest) request;
+ if (HttpMethod.GET.equalsIgnoreCase(httpRequest.getMethod())) {
+ // We don't create a session with this call as we need to check if there is a session already (i.e. if a user is logged in).
+ HttpSession session = httpRequest.getSession(false);
+ if (session != null) {
+ String csrfToken = (String) session.getAttribute(WebConstants.CSRF_TOKEN);
+ if (csrfToken == null) {
+ csrfToken = WebUtils.generateCsrfToken();
+ httpRequest.getSession().setAttribute(WebConstants.CSRF_TOKEN, csrfToken);
+ }
+ }
+ }
+ chain.doFilter(request, response);
+ }
+
+ @Override
+ public void destroy() {
+ }
+}
diff --git a/drill-yarn/src/main/java/org/apache/drill/yarn/appMaster/http/CsrfTokenValidateFilter.java b/drill-yarn/src/main/java/org/apache/drill/yarn/appMaster/http/CsrfTokenValidateFilter.java
new file mode 100644
index 0000000..6e00e21
--- /dev/null
+++ b/drill-yarn/src/main/java/org/apache/drill/yarn/appMaster/http/CsrfTokenValidateFilter.java
@@ -0,0 +1,63 @@
+/*
+ * 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.drill.yarn.appMaster.http;
+
+import org.apache.drill.exec.server.rest.WebServerConstants;
+import org.apache.drill.exec.server.rest.WebUtils;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.ws.rs.HttpMethod;
+import java.io.IOException;
+
+/**
+ * All forms on site have a field with a CSRF token injected by server.
+ * This filter compares the token with one stored in the session to ensure that the POST request is not a CSRF attack.
+ */
+public class CsrfTokenValidateFilter implements Filter {
+
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException {
+ }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+ HttpServletRequest httpRequest = (HttpServletRequest) request;
+ HttpServletResponse httpResponse = (HttpServletResponse) response;
+ if (HttpMethod.POST.equalsIgnoreCase(httpRequest.getMethod())) {
+ String csrfTokenSession = WebUtils.getCsrfTokenFromHttpRequest(httpRequest);
+ String csrfTokenRequest = httpRequest.getParameter(WebServerConstants.CSRF_TOKEN);
+ if (csrfTokenSession != null && !csrfTokenSession.equals(csrfTokenRequest)) {
+ httpResponse.reset();
+ httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "Potential CSRF detected");
+ return;
+ }
+ }
+ chain.doFilter(request, response);
+ }
+
+ @Override
+ public void destroy() {
+ }
+}
diff --git a/drill-yarn/src/main/java/org/apache/drill/yarn/appMaster/http/PageTree.java b/drill-yarn/src/main/java/org/apache/drill/yarn/appMaster/http/PageTree.java
index e4d5dc1..0308883 100644
--- a/drill-yarn/src/main/java/org/apache/drill/yarn/appMaster/http/PageTree.java
+++ b/drill-yarn/src/main/java/org/apache/drill/yarn/appMaster/http/PageTree.java
@@ -20,6 +20,7 @@
import java.util.HashMap;
import java.util.Map;
+import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.SecurityContext;
import org.apache.drill.exec.server.rest.auth.DrillUserPrincipal;
@@ -58,8 +59,15 @@
*/
public static Map<String, Object> toModel(SecurityContext sc, Object base) {
+ return toModel(sc, base, null);
+ }
+
+ public static Map<String, Object> toModel(SecurityContext sc, Object base, HttpServletRequest request) {
Map<String, Object> model = new HashMap<>();
model.put("model", base);
+ if (request != null) {
+ model.put(WebConstants.CSRF_TOKEN, WebUtils.getCsrfTokenFromHttpRequest(request));
+ }
return toMapModel(sc, model);
}
diff --git a/drill-yarn/src/main/java/org/apache/drill/yarn/appMaster/http/WebConstants.java b/drill-yarn/src/main/java/org/apache/drill/yarn/appMaster/http/WebConstants.java
new file mode 100644
index 0000000..365af2c
--- /dev/null
+++ b/drill-yarn/src/main/java/org/apache/drill/yarn/appMaster/http/WebConstants.java
@@ -0,0 +1,29 @@
+/*
+ * 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.drill.yarn.appMaster.http;
+
+/**
+ * Holds various constants used by WebServer components.
+ */
+public final class WebConstants {
+
+ private WebConstants() {}
+
+ // Name of the CSRF protection token attribute
+ public static final String CSRF_TOKEN = "csrfToken";
+}
diff --git a/drill-yarn/src/main/java/org/apache/drill/yarn/appMaster/http/WebServer.java b/drill-yarn/src/main/java/org/apache/drill/yarn/appMaster/http/WebServer.java
index 75d99d9..dd10b70 100644
--- a/drill-yarn/src/main/java/org/apache/drill/yarn/appMaster/http/WebServer.java
+++ b/drill-yarn/src/main/java/org/apache/drill/yarn/appMaster/http/WebServer.java
@@ -28,8 +28,10 @@
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.Date;
+import java.util.EnumSet;
import java.util.Set;
+import javax.servlet.DispatcherType;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
@@ -38,6 +40,8 @@
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
+import org.apache.drill.exec.server.rest.CsrfTokenInjectFilter;
+import org.apache.drill.exec.server.rest.CsrfTokenValidateFilter;
import org.apache.drill.yarn.appMaster.Dispatcher;
import org.apache.drill.yarn.core.DrillOnYarnConfig;
import org.bouncycastle.asn1.x500.X500NameBuilder;
@@ -167,6 +171,13 @@
restHolder.setInitOrder(2);
servletContextHandler.addServlet(restHolder, "/rest/*");
+ // Applying filters for CSRF protection.
+
+ servletContextHandler.addFilter(CsrfTokenInjectFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
+ for (String path : new String[]{"/resize", "/stop", "/cancel"}) {
+ servletContextHandler.addFilter(CsrfTokenValidateFilter.class, path, EnumSet.of(DispatcherType.REQUEST));
+ }
+
// Static resources (CSS, images, etc.)
setupStaticResources(servletContextHandler);
diff --git a/drill-yarn/src/main/java/org/apache/drill/yarn/appMaster/http/WebUiPageTree.java b/drill-yarn/src/main/java/org/apache/drill/yarn/appMaster/http/WebUiPageTree.java
index 58a59d3..8ad01ca 100644
--- a/drill-yarn/src/main/java/org/apache/drill/yarn/appMaster/http/WebUiPageTree.java
+++ b/drill-yarn/src/main/java/org/apache/drill/yarn/appMaster/http/WebUiPageTree.java
@@ -244,6 +244,9 @@
@Inject
private SecurityContext sc;
+ @Inject
+ private HttpServletRequest request;
+
@QueryParam("id")
private int id;
@@ -257,7 +260,7 @@
confirm = new ConfirmShrink(ConfirmShrink.Mode.CANCEL);
}
confirm.id = id;
- return new Viewable("/drill-am/shrink-warning.ftl", toModel(sc, confirm));
+ return new Viewable("/drill-am/shrink-warning.ftl", toModel(sc, confirm, request));
}
@POST
@@ -309,12 +312,15 @@
@Inject
SecurityContext sc;
+ @Inject
+ HttpServletRequest request;
+
@GET
@Produces(MediaType.TEXT_HTML)
public Viewable getRoot() {
ControllerModel model = new ControllerModel();
dispatcher.getController().visit(model);
- return new Viewable("/drill-am/manage.ftl", toModel(sc, model));
+ return new Viewable("/drill-am/manage.ftl", toModel(sc, model, request));
}
}
@@ -406,6 +412,9 @@
@Inject
SecurityContext sc;
+ @Inject
+ HttpServletRequest request;
+
@FormParam("n")
int n;
@FormParam("type")
@@ -453,7 +462,7 @@
ConfirmShrink confirm = new ConfirmShrink(ConfirmShrink.Mode.SHRINK);
confirm.value = curSize - newSize;
return new Viewable("/drill-am/shrink-warning.ftl",
- toModel(sc, confirm));
+ toModel(sc, confirm, request));
}
}
}
@@ -472,11 +481,14 @@
@Inject
SecurityContext sc;
+ @Inject
+ HttpServletRequest request;
+
@GET
@Produces(MediaType.TEXT_HTML)
public Viewable requestStop() {
ConfirmShrink confirm = new ConfirmShrink(ConfirmShrink.Mode.STOP);
- return new Viewable("/drill-am/shrink-warning.ftl", toModel(sc, confirm));
+ return new Viewable("/drill-am/shrink-warning.ftl", toModel(sc, confirm, request));
}
@POST
diff --git a/drill-yarn/src/main/java/org/apache/drill/yarn/appMaster/http/WebUtils.java b/drill-yarn/src/main/java/org/apache/drill/yarn/appMaster/http/WebUtils.java
new file mode 100644
index 0000000..246f3a6
--- /dev/null
+++ b/drill-yarn/src/main/java/org/apache/drill/yarn/appMaster/http/WebUtils.java
@@ -0,0 +1,51 @@
+/*
+ * 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.drill.yarn.appMaster.http;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+import java.security.SecureRandom;
+import java.util.Base64;
+
+public class WebUtils {
+
+ /**
+ * Retrieves the CSRF protection token from the HTTP request.
+ *
+ * @param request HTTP request that contains a session that stores a CSRF protection token.
+ * If there is no session, that means that authentication is disabled.
+ * @return CSRF protection token, or an empty string if there is no session present.
+ */
+ public static String getCsrfTokenFromHttpRequest(HttpServletRequest request) {
+ // No need to create a session if not present (i.e. if a user is logged in)
+ HttpSession session = request.getSession(false);
+ return session == null ? "" : (String) session.getAttribute(WebConstants.CSRF_TOKEN);
+ }
+
+ /**
+ * Generates a BASE64 encoded CSRF token from randomly generated 256-bit buffer
+ * according to the <a href="https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html">OWASP CSRF Prevention Cheat Sheet</a>
+ *
+ * @return randomly generated CSRF token.
+ */
+ public static String generateCsrfToken() {
+ byte[] buffer = new byte[32];
+ new SecureRandom().nextBytes(buffer);
+ return Base64.getUrlEncoder().withoutPadding().encodeToString(buffer);
+ }
+}
diff --git a/drill-yarn/src/main/java/org/apache/drill/yarn/client/DrillOnYarn.java b/drill-yarn/src/main/java/org/apache/drill/yarn/client/DrillOnYarn.java
index 587766a..54900d3 100644
--- a/drill-yarn/src/main/java/org/apache/drill/yarn/client/DrillOnYarn.java
+++ b/drill-yarn/src/main/java/org/apache/drill/yarn/client/DrillOnYarn.java
@@ -18,6 +18,8 @@
package org.apache.drill.yarn.client;
+import org.apache.drill.common.util.GuavaPatcher;
+import org.apache.drill.common.util.ProtobufPatcher;
import org.apache.drill.yarn.core.DoyConfigException;
import org.apache.drill.yarn.core.DrillOnYarnConfig;
import org.apache.log4j.BasicConfigurator;
@@ -73,6 +75,22 @@
*/
public class DrillOnYarn {
+
+ static {
+ /*
+ * Drill-on-YARN uses Hadoop dependencies that use older version of protobuf,
+ * and override some methods that became final in recent protobuf versions.
+ * This code removes these final modifiers.
+ */
+ ProtobufPatcher.patch();
+ /*
+ * HBase client uses older version of Guava's Stopwatch API,
+ * while Drill ships with 18.x which has changes the scope of
+ * these API to 'package', this code make them accessible.
+ */
+ GuavaPatcher.patch();
+ }
+
public static void main(String argv[]) {
BasicConfigurator.configure();
ClientContext.init();
@@ -107,35 +125,35 @@
ClientCommand cmd;
switch (opts.getCommand()) {
- case UPLOAD:
- cmd = new StartCommand(true, false);
- break;
- case START:
- cmd = new StartCommand(true, true);
- break;
- // Removed at QA request. QA wants a "real" restart. Also, upload of the
- // archive is fast enough that a "start without upload" option is not really
- // needed.
+ case UPLOAD:
+ cmd = new StartCommand(true, false);
+ break;
+ case START:
+ cmd = new StartCommand(true, true);
+ break;
+ // Removed at QA request. QA wants a "real" restart. Also, upload of the
+ // archive is fast enough that a "start without upload" option is not really
+ // needed.
// case RESTART:
// cmd = new StartCommand(false, true);
// break;
- case DESCRIBE:
- cmd = new PrintConfigCommand();
- break;
- case STATUS:
- cmd = new StatusCommand();
- break;
- case STOP:
- cmd = new StopCommand();
- break;
- case CLEAN:
- cmd = new CleanCommand();
- break;
- case RESIZE:
- cmd = new ResizeCommand();
- break;
- default:
- cmd = new HelpCommand();
+ case DESCRIBE:
+ cmd = new PrintConfigCommand();
+ break;
+ case STATUS:
+ cmd = new StatusCommand();
+ break;
+ case STOP:
+ cmd = new StopCommand();
+ break;
+ case CLEAN:
+ cmd = new CleanCommand();
+ break;
+ case RESIZE:
+ cmd = new ResizeCommand();
+ break;
+ default:
+ cmd = new HelpCommand();
}
// Run the command.
diff --git a/drill-yarn/src/main/resources/drill-am/manage.ftl b/drill-yarn/src/main/resources/drill-am/manage.ftl
index 2c26143..be16385 100644
--- a/drill-yarn/src/main/resources/drill-am/manage.ftl
+++ b/drill-yarn/src/main/resources/drill-am/manage.ftl
@@ -69,6 +69,7 @@
drillbits.
<button type="submit" class="btn btn-primary" style="margin: 0 1em;">Go</button>
</div>
+ <input type="hidden" name="csrfToken" value="${csrfToken}">
</form>
</td></tr>
<tr><td>
diff --git a/drill-yarn/src/main/resources/drill-am/shrink-warning.ftl b/drill-yarn/src/main/resources/drill-am/shrink-warning.ftl
index 3f5a9e3..4b0bbcc 100644
--- a/drill-yarn/src/main/resources/drill-am/shrink-warning.ftl
+++ b/drill-yarn/src/main/resources/drill-am/shrink-warning.ftl
@@ -60,6 +60,7 @@
</#if>
<input type="submit" value="Confirm"> or
<a href="/">Cancel</a>.
+ <input type="hidden" name="csrfToken" value="${csrfToken}">
</form>
</#macro>
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/CsrfTokenInjectFilter.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/CsrfTokenInjectFilter.java
new file mode 100644
index 0000000..dcedab2
--- /dev/null
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/CsrfTokenInjectFilter.java
@@ -0,0 +1,60 @@
+/*
+ * 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.drill.exec.server.rest;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+import javax.ws.rs.HttpMethod;
+import java.io.IOException;
+
+/**
+ * Generates and adds a CSRF token to a HTTP session. It is used to prevent CSRF attacks.
+ */
+public class CsrfTokenInjectFilter implements Filter {
+
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException {
+ }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+ HttpServletRequest httpRequest = (HttpServletRequest) request;
+ if (HttpMethod.GET.equalsIgnoreCase(httpRequest.getMethod())) {
+ // We don't create a session with this call as we need to check if there is a session already (i.e. if a user is logged in).
+ HttpSession session = httpRequest.getSession(false);
+ if (session != null) {
+ String csrfToken = (String) session.getAttribute(WebServerConstants.CSRF_TOKEN);
+ if (csrfToken == null) {
+ csrfToken = WebUtils.generateCsrfToken();
+ httpRequest.getSession().setAttribute(WebServerConstants.CSRF_TOKEN, csrfToken);
+ }
+ }
+ }
+ chain.doFilter(request, response);
+ }
+
+ @Override
+ public void destroy() {
+ }
+}
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/CsrfTokenValidateFilter.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/CsrfTokenValidateFilter.java
new file mode 100644
index 0000000..439d89f
--- /dev/null
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/CsrfTokenValidateFilter.java
@@ -0,0 +1,61 @@
+/*
+ * 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.drill.exec.server.rest;
+
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.ws.rs.HttpMethod;
+import java.io.IOException;
+
+/**
+ * All forms on site have a field with a CSRF token injected by server.
+ * This filter compares the token with one stored in the session to ensure that the POST request is not a CSRF attack.
+ */
+public class CsrfTokenValidateFilter implements Filter {
+
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException {
+ }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+ HttpServletRequest httpRequest = (HttpServletRequest) request;
+ HttpServletResponse httpResponse = (HttpServletResponse) response;
+ if (HttpMethod.POST.equalsIgnoreCase(httpRequest.getMethod())) {
+ String csrfTokenSession = WebUtils.getCsrfTokenFromHttpRequest(httpRequest);
+ String csrfTokenRequest = httpRequest.getParameter(WebServerConstants.CSRF_TOKEN);
+ if (csrfTokenSession != null && !csrfTokenSession.equals(csrfTokenRequest)) {
+ httpResponse.reset();
+ httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "Potential CSRF detected");
+ return;
+ }
+ }
+ chain.doFilter(request, response);
+ }
+
+ @Override
+ public void destroy() {
+ }
+}
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/QueryResources.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/QueryResources.java
index 60746e9..75d6653 100644
--- a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/QueryResources.java
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/QueryResources.java
@@ -28,9 +28,12 @@
import org.apache.drill.exec.server.rest.QueryWrapper.QueryResult;
import org.apache.drill.exec.work.WorkManager;
import org.glassfish.jersey.server.mvc.Viewable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import javax.annotation.security.RolesAllowed;
import javax.inject.Inject;
+import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
@@ -47,13 +50,22 @@
@Path("/")
@RolesAllowed(DrillUserPrincipal.AUTHENTICATED_ROLE)
public class QueryResources {
- static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(QueryResources.class);
+ private static final Logger logger = LoggerFactory.getLogger(QueryResources.class);
- @Inject UserAuthEnabled authEnabled;
- @Inject WorkManager work;
- @Inject SecurityContext sc;
- @Inject WebUserConnection webUserConnection;
+ @Inject
+ UserAuthEnabled authEnabled;
+ @Inject
+ WorkManager work;
+
+ @Inject
+ SecurityContext sc;
+
+ @Inject
+ WebUserConnection webUserConnection;
+
+ @Inject
+ HttpServletRequest request;
@GET
@Path("/query")
@@ -61,7 +73,7 @@
public Viewable getQuery() {
return ViewableWithPermissions.create(
authEnabled.get(), "/rest/query/query.ftl",
- sc, new QueryPage(work));
+ sc, new QueryPage(work, request));
}
@POST
@@ -105,13 +117,15 @@
private final boolean onlyImpersonationEnabled;
private final boolean autoLimitEnabled;
private final int defaultRowsAutoLimited;
+ private final String csrfToken;
- public QueryPage(WorkManager work) {
+ public QueryPage(WorkManager work, HttpServletRequest request) {
DrillConfig config = work.getContext().getConfig();
//if impersonation is enabled without authentication, will provide mechanism to add user name to request header from Web UI
onlyImpersonationEnabled = WebServer.isOnlyImpersonationEnabled(config);
autoLimitEnabled = config.getBoolean(ExecConstants.HTTP_WEB_CLIENT_RESULTSET_AUTOLIMIT_CHECKED);
defaultRowsAutoLimited = config.getInt(ExecConstants.HTTP_WEB_CLIENT_RESULTSET_AUTOLIMIT_ROWS);
+ csrfToken = WebUtils.getCsrfTokenFromHttpRequest(request);
}
public boolean isOnlyImpersonationEnabled() {
@@ -125,6 +139,10 @@
public int getDefaultRowsAutoLimited() {
return defaultRowsAutoLimited;
}
+
+ public String getCsrfToken() {
+ return csrfToken;
+ }
}
/**
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/StatusResources.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/StatusResources.java
index f57a8b5..a27d9c1 100644
--- a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/StatusResources.java
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/StatusResources.java
@@ -26,6 +26,7 @@
import javax.annotation.security.PermitAll;
import javax.annotation.security.RolesAllowed;
import javax.inject.Inject;
+import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
@@ -70,9 +71,17 @@
//Used to access current filter state in WebUI
private static final String CURRENT_FILTER_PARAM = "filter";
- @Inject UserAuthEnabled authEnabled;
- @Inject WorkManager work;
- @Inject SecurityContext sc;
+ @Inject
+ UserAuthEnabled authEnabled;
+
+ @Inject
+ WorkManager work;
+
+ @Inject
+ SecurityContext sc;
+
+ @Inject
+ HttpServletRequest request;
@GET
@Path(StatusResources.PATH_STATUS_JSON)
@@ -135,7 +144,7 @@
return ViewableWithPermissions.create(authEnabled.get(),
"/rest/options.ftl",
sc,
- new OptionsListing(options, fltrList, currFilter));
+ new OptionsListing(options, fltrList, currFilter, request));
}
@GET
@@ -185,22 +194,30 @@
private final List<OptionWrapper> options;
private final List<String> filters;
private final String dynamicFilter;
+ private final String csrfToken;
- public OptionsListing(List<OptionWrapper> optList, List<String> fltrList, String currFilter) {
+ public OptionsListing(List<OptionWrapper> optList, List<String> fltrList, String currFilter, HttpServletRequest request) {
this.options = optList;
this.filters = fltrList;
this.dynamicFilter = currFilter;
+ csrfToken = WebUtils.getCsrfTokenFromHttpRequest(request);
}
public List<OptionWrapper> getOptions() {
return options;
}
+
public List<String> getFilters() {
return filters;
}
+
public String getDynamicFilter() {
return dynamicFilter;
}
+
+ public String getCsrfToken() {
+ return csrfToken;
+ }
}
@XmlRootElement
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/StorageResources.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/StorageResources.java
index c3de7f5..f1ccc2f 100644
--- a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/StorageResources.java
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/StorageResources.java
@@ -30,6 +30,7 @@
import javax.annotation.security.RolesAllowed;
import javax.inject.Inject;
+import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.FormParam;
@@ -56,18 +57,30 @@
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import static org.apache.drill.exec.server.rest.auth.DrillUserPrincipal.ADMIN_ROLE;
@Path("/")
@RolesAllowed(ADMIN_ROLE)
public class StorageResources {
- static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(StorageResources.class);
+ private static final Logger logger = LoggerFactory.getLogger(StorageResources.class);
- @Inject UserAuthEnabled authEnabled;
- @Inject StoragePluginRegistry storage;
- @Inject ObjectMapper mapper;
- @Inject SecurityContext sc;
+ @Inject
+ UserAuthEnabled authEnabled;
+
+ @Inject
+ StoragePluginRegistry storage;
+
+ @Inject
+ ObjectMapper mapper;
+
+ @Inject
+ SecurityContext sc;
+
+ @Inject
+ HttpServletRequest request;
private static final String JSON_FORMAT = "json";
private static final String HOCON_FORMAT = "conf";
@@ -112,8 +125,14 @@
@Path("/storage")
@Produces(MediaType.TEXT_HTML)
public Viewable getPlugins() {
- List<PluginConfigWrapper> list = getPluginsJSON();
- return ViewableWithPermissions.create(authEnabled.get(), "/rest/storage/list.ftl", sc, list);
+ List<StoragePluginModel> model = getPluginsJSON().stream()
+ .map(plugin -> new StoragePluginModel(plugin, request))
+ .collect(Collectors.toList());
+ // Creating an empty model with CSRF token, if there are no storage plugins
+ if (model.size() == 0) {
+ model.add(new StoragePluginModel(null, request));
+ }
+ return ViewableWithPermissions.create(authEnabled.get(), "/rest/storage/list.ftl", sc, model);
}
@GET
@@ -135,8 +154,9 @@
@Path("/storage/{name}")
@Produces(MediaType.TEXT_HTML)
public Viewable getPlugin(@PathParam("name") String name) {
+ StoragePluginModel model = new StoragePluginModel(getPluginConfig(name), request);
return ViewableWithPermissions.create(authEnabled.get(), "/rest/storage/update.ftl", sc,
- getPluginConfig(name));
+ model);
}
@GET
@@ -301,4 +321,26 @@
}
}
+
+ /**
+ * Model class for Storage Plugin page.
+ * It contains a storage plugin as well as the CSRK token for the page.
+ */
+ public static class StoragePluginModel {
+ private final PluginConfigWrapper plugin;
+ private final String csrfToken;
+
+ public StoragePluginModel(PluginConfigWrapper plugin, HttpServletRequest request) {
+ this.plugin = plugin;
+ csrfToken = WebUtils.getCsrfTokenFromHttpRequest(request);
+ }
+
+ public PluginConfigWrapper getPlugin() {
+ return plugin;
+ }
+
+ public String getCsrfToken() {
+ return csrfToken;
+ }
+ }
}
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/WebServer.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/WebServer.java
index bc093ad..90fd629 100644
--- a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/WebServer.java
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/WebServer.java
@@ -242,6 +242,12 @@
servletContextHandler.setSessionHandler(createSessionHandler(servletContextHandler.getSecurityHandler()));
}
+ // Applying filters for CSRF protection.
+ servletContextHandler.addFilter(CsrfTokenInjectFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
+ for (String path : new String[]{"/query", "/storage/create_update", "/option/*"}) {
+ servletContextHandler.addFilter(CsrfTokenValidateFilter.class, path, EnumSet.of(DispatcherType.REQUEST));
+ }
+
if (isOnlyImpersonationEnabled(workManager.getContext().getConfig())) {
for (String path : new String[]{"/query", "/query.json"}) {
servletContextHandler.addFilter(UserNameFilter.class, path, EnumSet.of(DispatcherType.REQUEST));
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/WebServerConstants.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/WebServerConstants.java
index 5cd1c5f..f64aa3c 100644
--- a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/WebServerConstants.java
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/WebServerConstants.java
@@ -42,4 +42,7 @@
// Logout page
public static final String LOGOUT_RESOURCE_NAME = "logout";
public static final String LOGOUT_RESOURCE_PATH = WEBSERVER_ROOT_PATH + LOGOUT_RESOURCE_NAME;
-}
\ No newline at end of file
+
+ // Name of the CSRF protection token attribute
+ public static final String CSRF_TOKEN = "csrfToken";
+}
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/WebUtils.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/WebUtils.java
new file mode 100644
index 0000000..4e1b99a
--- /dev/null
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/WebUtils.java
@@ -0,0 +1,51 @@
+/*
+ * 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.drill.exec.server.rest;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+import java.security.SecureRandom;
+import java.util.Base64;
+
+public class WebUtils {
+
+ /**
+ * Retrieves the CSRF protection token from the HTTP request.
+ *
+ * @param request HTTP request that contains a session that stores a CSRF protection token.
+ * If there is no session, that means that authentication is disabled.
+ * @return CSRF protection token, or an empty string if there is no session present.
+ */
+ public static String getCsrfTokenFromHttpRequest(HttpServletRequest request) {
+ // No need to create a session if not present (i.e. if a user is logged in)
+ HttpSession session = request.getSession(false);
+ return session == null ? "" : (String) session.getAttribute(WebServerConstants.CSRF_TOKEN);
+ }
+
+ /**
+ * Generates a BASE64 encoded CSRF token from randomly generated 256-bit buffer
+ * according to the <a href="https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html">OWASP CSRF Prevention Cheat Sheet</a>
+ *
+ * @return randomly generated CSRF token.
+ */
+ public static String generateCsrfToken() {
+ byte[] buffer = new byte[32];
+ new SecureRandom().nextBytes(buffer);
+ return Base64.getUrlEncoder().withoutPadding().encodeToString(buffer);
+ }
+}
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/profile/ProfileResources.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/profile/ProfileResources.java
index 1819962..e14e696 100644
--- a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/profile/ProfileResources.java
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/profile/ProfileResources.java
@@ -27,6 +27,7 @@
import javax.annotation.security.RolesAllowed;
import javax.inject.Inject;
+import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
@@ -65,10 +66,20 @@
public class ProfileResources {
private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(ProfileResources.class);
- @Inject UserAuthEnabled authEnabled;
- @Inject WorkManager work;
- @Inject DrillUserPrincipal principal;
- @Inject SecurityContext sc;
+ @Inject
+ UserAuthEnabled authEnabled;
+
+ @Inject
+ WorkManager work;
+
+ @Inject
+ DrillUserPrincipal principal;
+
+ @Inject
+ SecurityContext sc;
+
+ @Inject
+ HttpServletRequest request;
public static class ProfileInfo implements Comparable<ProfileInfo> {
private static final int QUERY_SNIPPET_MAX_CHAR = 150;
@@ -375,7 +386,7 @@
@Produces(MediaType.TEXT_HTML)
public Viewable getProfile(@PathParam("queryid") String queryId){
try {
- ProfileWrapper wrapper = new ProfileWrapper(getQueryProfile(queryId), work.getContext().getConfig());
+ ProfileWrapper wrapper = new ProfileWrapper(getQueryProfile(queryId), work.getContext().getConfig(), request);
return ViewableWithPermissions.create(authEnabled.get(), "/rest/profile/profile.ftl", sc, wrapper);
} catch (Exception | Error e) {
logger.error("Exception was thrown when fetching profile {} :\n{}", queryId, e);
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/profile/ProfileWrapper.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/profile/ProfileWrapper.java
index 5c144eb..e5c4133 100644
--- a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/profile/ProfileWrapper.java
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/profile/ProfileWrapper.java
@@ -41,10 +41,13 @@
import org.apache.drill.exec.server.options.OptionList;
import org.apache.drill.exec.server.options.OptionValue;
import org.apache.drill.exec.server.rest.WebServer;
+import org.apache.drill.exec.server.rest.WebUtils;
import org.apache.drill.shaded.guava.com.google.common.base.CaseFormat;
import com.fasterxml.jackson.databind.ObjectMapper;
+import javax.servlet.http.HttpServletRequest;
+
/**
* Wrapper class for a {@link #profile query profile}, so it to be presented through web UI.
*/
@@ -66,14 +69,16 @@
private final String noProgressWarningThreshold;
private final int defaultAutoLimit;
private boolean showEstimatedRows;
+ private final String csrfToken;
- public ProfileWrapper(final QueryProfile profile, DrillConfig drillConfig) {
+ public ProfileWrapper(final QueryProfile profile, DrillConfig drillConfig, HttpServletRequest request) {
this.profile = profile;
this.id = profile.hasQueryId() ? profile.getQueryId() : QueryIdHelper.getQueryId(profile.getId());
this.defaultAutoLimit = drillConfig.getInt(ExecConstants.HTTP_WEB_CLIENT_RESULTSET_AUTOLIMIT_ROWS);
//Generating Operator Name map (DRILL-6140)
String profileTextPlan = profile.hasPlan()? profile.getPlan(): "";
generateOpMap(profileTextPlan);
+ csrfToken = WebUtils.getCsrfTokenFromHttpRequest(request);
final List<FragmentWrapper> fragmentProfiles = new ArrayList<>();
@@ -280,10 +285,10 @@
}
//Threshold to be used by WebServer in issuing warning
+
public String getNoProgressWarningThreshold() {
return this.noProgressWarningThreshold;
}
-
public List<FragmentWrapper> getFragmentProfiles() {
return fragmentProfiles;
}
@@ -373,6 +378,7 @@
}
//Generates operator names inferred from physical plan
+
private void generateOpMap(String plan) {
this.physicalOperatorMap = new HashMap<>();
if (plan.isEmpty()) {
@@ -392,8 +398,11 @@
physicalOperatorMap.put(operatorPath, extractedOperatorName);
}
}
-
public boolean showEstimatedRows() {
return showEstimatedRows;
}
+
+ public String getCsrfToken() {
+ return csrfToken;
+ }
}
diff --git a/exec/java-exec/src/main/resources/rest/options.ftl b/exec/java-exec/src/main/resources/rest/options.ftl
index d05dd7f..c01207d 100644
--- a/exec/java-exec/src/main/resources/rest/options.ftl
+++ b/exec/java-exec/src/main/resources/rest/options.ftl
@@ -33,7 +33,12 @@
} else { //Apply filter for updated field
redirectHref = redirectHref + "?filter=" + optionName;
}
- $.post("/option/"+optionName, {kind: optionKind, name: optionName, value: optionValue}, function () {
+ $.post("/option/"+optionName, {
+ kind: optionKind,
+ name: optionName,
+ value: optionValue,
+ csrfToken: "${model.getCsrfToken()}"
+ }, function () {
//Remove existing filters
location.href=redirectHref;
});
diff --git a/exec/java-exec/src/main/resources/rest/profile/profile.ftl b/exec/java-exec/src/main/resources/rest/profile/profile.ftl
index 303d83d..704c2dd 100644
--- a/exec/java-exec/src/main/resources/rest/profile/profile.ftl
+++ b/exec/java-exec/src/main/resources/rest/profile/profile.ftl
@@ -201,6 +201,7 @@
</button>
<input type="checkbox" name="forceLimit" value="limit" <#if model.hasAutoLimit()>checked</#if>> Limit results to <input type="text" id="autoLimit" name="autoLimit" min="0" value="<#if model.hasAutoLimit()>${model.getAutoLimit()?c}<#else>${model.getDefaultAutoLimit()?c}</#if>" size="6" pattern="[0-9]*"> rows <span class="glyphicon glyphicon-info-sign" title="Limits the number of records retrieved in the query. Ignored if query has a limit already" style="cursor:pointer"></span>
</div>
+ <input type="hidden" name="csrfToken" value="${model.getCsrfToken()}">
</form>
</p>
diff --git a/exec/java-exec/src/main/resources/rest/query/query.ftl b/exec/java-exec/src/main/resources/rest/query/query.ftl
index 8a4942c..aea8bb3 100644
--- a/exec/java-exec/src/main/resources/rest/query/query.ftl
+++ b/exec/java-exec/src/main/resources/rest/query/query.ftl
@@ -83,6 +83,7 @@
Submit
</button>
<input type="checkbox" name="forceLimit" value="limit" <#if model.isAutoLimitEnabled()>checked</#if>> Limit results to <input type="text" id="autoLimit" name="autoLimit" min="0" value="${model.getDefaultRowsAutoLimited()?c}" size="6" pattern="[0-9]*"> rows <span class="glyphicon glyphicon-info-sign" title="Limits the number of records retrieved in the query. Ignored if query has a limit already" style="cursor:pointer"></span>
+ <input type="hidden" name="csrfToken" value="${model.getCsrfToken()}">
</form>
<script>
diff --git a/exec/java-exec/src/main/resources/rest/storage/list.ftl b/exec/java-exec/src/main/resources/rest/storage/list.ftl
index f79485c..3bf547e 100644
--- a/exec/java-exec/src/main/resources/rest/storage/list.ftl
+++ b/exec/java-exec/src/main/resources/rest/storage/list.ftl
@@ -56,20 +56,20 @@
<h4>Enabled Storage Plugins</h4>
<table class="table table-hover">
<tbody>
- <#list model as plugin>
- <#if plugin.enabled() == true>
+ <#list model as pluginModel>
+ <#if pluginModel.getPlugin()?? && pluginModel.getPlugin().enabled() == true>
<tr>
<td style="border:none; max-width: 200px; overflow: hidden; text-overflow: ellipsis;">
- ${plugin.getName()}
+ ${pluginModel.getPlugin().getName()}
</td>
<td style="border:none;">
- <button type="button" class="btn btn-primary" onclick="doUpdate('${plugin.getName()}')">
+ <button type="button" class="btn btn-primary" onclick="doUpdate('${pluginModel.getPlugin().getName()}')">
Update
</button>
- <button type="button" class="btn btn-warning" onclick="doEnable('${plugin.getName()}', false)">
+ <button type="button" class="btn btn-warning" onclick="doEnable('${pluginModel.getPlugin().getName()}', false)">
Disable
</button>
- <button type="button" class="btn" name="${plugin.getName()}" data-toggle="modal"
+ <button type="button" class="btn" name="${pluginModel.getPlugin().getName()}" data-toggle="modal"
data-target="#pluginsModal">
Export
</button>
@@ -85,20 +85,20 @@
<h4>Disabled Storage Plugins</h4>
<table class="table table-hover">
<tbody>
- <#list model as plugin>
- <#if plugin.enabled() == false>
+ <#list model as pluginModel>
+ <#if pluginModel.getPlugin()?? && pluginModel.getPlugin().enabled() == false>
<tr>
<td style="border:none; max-width: 200px; overflow: hidden; text-overflow: ellipsis;">
- ${plugin.getName()}
+ ${pluginModel.getPlugin().getName()}
</td>
<td style="border:none;">
- <button type="button" class="btn btn-primary" onclick="doUpdate('${plugin.getName()}')">
+ <button type="button" class="btn btn-primary" onclick="doUpdate('${pluginModel.getPlugin().getName()}')">
Update
</button>
- <button type="button" class="btn btn-success" onclick="doEnable('${plugin.getName()}', true)">
+ <button type="button" class="btn btn-success" onclick="doEnable('${pluginModel.getPlugin().getName()}', true)">
Enable
</button>
- <button type="button" class="btn" name="${plugin.getName()}" data-toggle="modal"
+ <button type="button" class="btn" name="${pluginModel.getPlugin().getName()}" data-toggle="modal"
data-target="#pluginsModal">
Export
</button>
@@ -190,6 +190,7 @@
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary" onclick="doCreate()">Create</button>
</div>
+ <input type="hidden" name="csrfToken" value="${model[0].getCsrfToken()}">
</form>
<div id="message" class="hidden alert alert-info">
diff --git a/exec/java-exec/src/main/resources/rest/storage/update.ftl b/exec/java-exec/src/main/resources/rest/storage/update.ftl
index d077ea0..9d7b577 100644
--- a/exec/java-exec/src/main/resources/rest/storage/update.ftl
+++ b/exec/java-exec/src/main/resources/rest/storage/update.ftl
@@ -32,7 +32,7 @@
</div>
<h3>Configuration</h3>
<form id="updateForm" role="form" action="/storage/create_update" method="POST">
- <input type="hidden" name="name" value="${model.getName()}" />
+ <input type="hidden" name="name" value="${model.getPlugin().getName()}" />
<div class="form-group">
<div id="editor" class="form-control"></div>
<textarea class="form-control" id="config" name="config" data-editor="json" style="display: none;" >
@@ -40,16 +40,17 @@
</div>
<a class="btn btn-default" href="/storage">Back</a>
<button class="btn btn-default" type="submit" onclick="doUpdate();">Update</button>
- <#if model.enabled()>
+ <#if model.getPlugin().enabled()>
<a id="enabled" class="btn btn-default">Disable</a>
<#else>
<a id="enabled" class="btn btn-primary">Enable</a>
</#if>
- <button type="button" class="btn btn-default export" name="${model.getName()}" data-toggle="modal"
+ <button type="button" class="btn btn-default export" name="${model.getPlugin().getName()}" data-toggle="modal"
data-target="#pluginsModal">
Export
</button>
<a id="del" class="btn btn-danger" onclick="deleteFunction()">Delete</a>
+ <input type="hidden" name="csrfToken" value="${model.getCsrfToken()}">
</form>
<br>
<div id="message" class="hidden alert alert-info">
@@ -108,21 +109,21 @@
textarea.val(editor.getSession().getValue());
});
- $.get("/storage/" + encodeURIComponent("${model.getName()}") + ".json", function(data) {
+ $.get("/storage/" + encodeURIComponent("${model.getPlugin().getName()}") + ".json", function(data) {
$("#config").val(JSON.stringify(data.config, null, 2));
editor.getSession().setValue( JSON.stringify(data.config, null, 2) );
});
$("#enabled").click(function() {
- const enabled = ${model.enabled()?c};
+ const enabled = ${model.getPlugin().enabled()?c};
if (enabled) {
- showConfirmationDialog('"${model.getName()}"' + ' plugin will be disabled. Proceed?', proceed);
+ showConfirmationDialog('"${model.getPlugin().getName()}"' + ' plugin will be disabled. Proceed?', proceed);
} else {
proceed();
}
function proceed() {
- $.get("/storage/" + encodeURIComponent("${model.getName()}") + "/enable/<#if model.enabled()>false<#else>true</#if>", function(data) {
+ $.get("/storage/" + encodeURIComponent("${model.getPlugin().getName()}") + "/enable/<#if model.getPlugin().enabled()>false<#else>true</#if>", function(data) {
$("#message").removeClass("hidden").text(data.result).alert();
setTimeout(function() { location.reload(); }, 800);
});
@@ -137,8 +138,8 @@
}
function deleteFunction() {
- showConfirmationDialog('"${model.getName()}"' + ' plugin will be deleted. Proceed?', function() {
- $.get("/storage/" + encodeURIComponent("${model.getName()}") + "/delete", serverMessage);
+ showConfirmationDialog('"${model.getPlugin().getName()}"' + ' plugin will be deleted. Proceed?', function() {
+ $.get("/storage/" + encodeURIComponent("${model.getPlugin().getName()}") + "/delete", serverMessage);
});
}