From 9648ebd71892daa996916f7f8492cc5a61d76ff8 Mon Sep 17 00:00:00 2001
From: Max Thonagel <12283268+thoniTUB@users.noreply.github.com>
Date: Thu, 25 Apr 2024 15:11:38 +0200
Subject: [PATCH 01/11] naive implementation of the csrf double submit pattern
Signed-off-by: Max Thonagel <12283268+thoniTUB@users.noreply.github.com>
---
.../models/auth/web/AuthCookieFilter.java | 6 +-
.../auth/web/csrf/CsrfTokenCheckFilter.java | 48 +++++++++++
.../auth/web/csrf/CsrfTokenSetFilter.java | 52 +++++++++++
.../resources/admin/AdminServlet.java | 8 +-
.../resources/admin/rest/UIProcessor.java | 4 +-
.../resources/admin/ui/AdminUIResource.java | 15 ++--
.../admin/ui/AuthOverviewUIResource.java | 7 +-
.../admin/ui/ConceptsUIResource.java | 7 +-
.../admin/ui/DatasetsUIResource.java | 19 ++--
.../resources/admin/ui/GroupUIResource.java | 19 ++--
.../admin/ui/IndexServiceUIResource.java | 18 ++--
.../resources/admin/ui/RoleUIResource.java | 10 ++-
.../resources/admin/ui/TablesUIResource.java | 9 +-
.../resources/admin/ui/UserUIResource.java | 10 ++-
.../admin/ui/model/ConnectorUIResource.java | 7 +-
.../resources/admin/ui/model/UIContext.java | 3 +
.../main/resources/assets/custom/js/script.js | 86 ++++++++++++++++++-
.../resources/admin/ui/dataset.html.ftl | 2 -
.../resources/admin/ui/datasets.html.ftl | 43 +---------
.../resources/admin/ui/scripts/dataset.js | 45 ----------
.../admin/ui/templates/base.html.ftl | 9 +-
21 files changed, 296 insertions(+), 131 deletions(-)
create mode 100644 backend/src/main/java/com/bakdata/conquery/models/auth/web/csrf/CsrfTokenCheckFilter.java
create mode 100644 backend/src/main/java/com/bakdata/conquery/models/auth/web/csrf/CsrfTokenSetFilter.java
delete mode 100644 backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/scripts/dataset.js
diff --git a/backend/src/main/java/com/bakdata/conquery/models/auth/web/AuthCookieFilter.java b/backend/src/main/java/com/bakdata/conquery/models/auth/web/AuthCookieFilter.java
index a6ab815dbf..015ec9634a 100644
--- a/backend/src/main/java/com/bakdata/conquery/models/auth/web/AuthCookieFilter.java
+++ b/backend/src/main/java/com/bakdata/conquery/models/auth/web/AuthCookieFilter.java
@@ -1,5 +1,7 @@
package com.bakdata.conquery.models.auth.web;
+import static com.bakdata.conquery.models.auth.web.AuthCookieFilter.PRIORITY;
+
import java.io.IOException;
import com.bakdata.conquery.models.config.ConqueryConfig;
@@ -31,10 +33,12 @@
@Slf4j
@PreMatching
// Chain this filter before the Authentication filter
-@Priority(Priorities.AUTHENTICATION-100)
+@Priority(PRIORITY)
@RequiredArgsConstructor(onConstructor_ = {@Inject})
public class AuthCookieFilter implements ContainerRequestFilter, ContainerResponseFilter {
+ public static final int PRIORITY = Priorities.AUTHENTICATION - 100;
+
public static final String ACCESS_TOKEN = "access_token";
private static final String PREFIX = "bearer";
diff --git a/backend/src/main/java/com/bakdata/conquery/models/auth/web/csrf/CsrfTokenCheckFilter.java b/backend/src/main/java/com/bakdata/conquery/models/auth/web/csrf/CsrfTokenCheckFilter.java
new file mode 100644
index 0000000000..e66f05322d
--- /dev/null
+++ b/backend/src/main/java/com/bakdata/conquery/models/auth/web/csrf/CsrfTokenCheckFilter.java
@@ -0,0 +1,48 @@
+package com.bakdata.conquery.models.auth.web.csrf;
+
+import java.io.IOException;
+import java.util.Optional;
+
+import com.bakdata.conquery.models.auth.web.AuthCookieFilter;
+import jakarta.annotation.Priority;
+import jakarta.ws.rs.ForbiddenException;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.container.ContainerRequestFilter;
+import jakarta.ws.rs.core.Cookie;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * Implementation of the Double-Submit-Cookie Pattern.
+ * Checks if tokens in cookie and header match if a cookie is present.
+ * Otherwise the request is refused.
+ */
+@Priority(AuthCookieFilter.PRIORITY - 100)
+@Slf4j
+public class CsrfTokenCheckFilter implements ContainerRequestFilter {
+ public static final String CSRF_TOKEN_HEADER = "X-Csrf-Token";
+
+ @Override
+ public void filter(ContainerRequestContext requestContext) throws IOException {
+ final String cookieToken = Optional.ofNullable(requestContext.getCookies().get(CsrfTokenSetFilter.CSRF_COOKIE_NAME)).map(Cookie::getValue).orElse(null);
+ final String headerToken = requestContext.getHeaders().getFirst(CSRF_TOKEN_HEADER);
+
+ if (cookieToken == null) {
+ log.trace("Request had no csrf token set. Accepting request");
+ return;
+ }
+
+ if (StringUtils.isBlank(headerToken)) {
+ log.warn("Request contained csrf cookie but the header token was empty");
+ throw new ForbiddenException("CSRF Attempt");
+ }
+
+ if (!cookieToken.equals(headerToken)) {
+ log.warn("Request csrf cookie and header did not match");
+ throw new ForbiddenException("CSRF Attempt");
+ }
+
+ log.trace("Csrf check successful");
+
+ }
+}
diff --git a/backend/src/main/java/com/bakdata/conquery/models/auth/web/csrf/CsrfTokenSetFilter.java b/backend/src/main/java/com/bakdata/conquery/models/auth/web/csrf/CsrfTokenSetFilter.java
new file mode 100644
index 0000000000..a0e7f47047
--- /dev/null
+++ b/backend/src/main/java/com/bakdata/conquery/models/auth/web/csrf/CsrfTokenSetFilter.java
@@ -0,0 +1,52 @@
+package com.bakdata.conquery.models.auth.web.csrf;
+
+import java.io.IOException;
+import java.security.SecureRandom;
+import java.util.Random;
+
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.container.ContainerRequestFilter;
+import jakarta.ws.rs.container.ContainerResponseContext;
+import jakarta.ws.rs.container.ContainerResponseFilter;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.NewCookie;
+import org.apache.commons.lang3.RandomStringUtils;
+
+/**
+ * Implementation of the Double-Submit-Cookie Pattern.
+ * This filter generates a random token which is injected in to the response.
+ *
+ *
In a Set-Cookie header, so that browser requests send the token via cookie back to us
+ *
In the response payload. This filter sets a request property, which is eventually provided to freemarker.
+ * Freemarker then writes the token into payload (see base.html.ftl)
+ *
+ */
+public class CsrfTokenSetFilter implements ContainerRequestFilter, ContainerResponseFilter {
+
+ public static final String CSRF_COOKIE_NAME = "csrf_token";
+ public static final String CSRF_TOKEN_PROPERTY = "csrf_token";
+ public static final int TOKEN_LENGTH = 30;
+
+ Random random = new SecureRandom();
+
+ @Override
+ public void filter(ContainerRequestContext requestContext) throws IOException {
+ final String token = RandomStringUtils.random(TOKEN_LENGTH, 0, 0, true, true,
+ null, random
+ );
+ requestContext.setProperty(CSRF_TOKEN_PROPERTY, token);
+ }
+
+ @Override
+ public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException {
+ final String csrfToken = getCsrfTokenProperty(requestContext);
+
+ responseContext.getHeaders()
+ .add(HttpHeaders.SET_COOKIE, new NewCookie(CSRF_COOKIE_NAME, csrfToken, "/", null, 0, null, 3600, null, requestContext.getSecurityContext()
+ .isSecure(), false));
+ }
+
+ public static String getCsrfTokenProperty(ContainerRequestContext requestContext) {
+ return (String) requestContext.getProperty(CSRF_TOKEN_PROPERTY);
+ }
+}
diff --git a/backend/src/main/java/com/bakdata/conquery/resources/admin/AdminServlet.java b/backend/src/main/java/com/bakdata/conquery/resources/admin/AdminServlet.java
index 8696ba7edd..28e8ecf939 100644
--- a/backend/src/main/java/com/bakdata/conquery/resources/admin/AdminServlet.java
+++ b/backend/src/main/java/com/bakdata/conquery/resources/admin/AdminServlet.java
@@ -13,6 +13,8 @@
import com.bakdata.conquery.io.jersey.RESTServer;
import com.bakdata.conquery.io.storage.MetaStorage;
import com.bakdata.conquery.models.auth.web.AuthCookieFilter;
+import com.bakdata.conquery.models.auth.web.csrf.CsrfTokenCheckFilter;
+import com.bakdata.conquery.models.auth.web.csrf.CsrfTokenSetFilter;
import com.bakdata.conquery.models.config.ConqueryConfig;
import com.bakdata.conquery.models.jobs.JobManager;
import com.bakdata.conquery.models.worker.DatasetRegistry;
@@ -120,7 +122,8 @@ protected void configure() {
.register(IdRefPathParamConverterProvider.class)
.register(new MultiPartFeature())
.register(IdParamConverter.Provider.INSTANCE)
- .register(AuthCookieFilter.class);
+ .register(AuthCookieFilter.class)
+ .register(CsrfTokenCheckFilter.class);
jerseyConfigUI.register(new ViewMessageBodyWriter(manager.getEnvironment().metrics(), Collections.singleton(Freemarker.HTML_RENDERER)))
@@ -136,7 +139,8 @@ protected void configure() {
})
.register(AdminPermissionFilter.class)
.register(IdRefPathParamConverterProvider.class)
- .register(AuthCookieFilter.class);
+ .register(AuthCookieFilter.class)
+ .register(CsrfTokenSetFilter.class);
}
diff --git a/backend/src/main/java/com/bakdata/conquery/resources/admin/rest/UIProcessor.java b/backend/src/main/java/com/bakdata/conquery/resources/admin/rest/UIProcessor.java
index 78b0448a2e..17c89ccdbf 100644
--- a/backend/src/main/java/com/bakdata/conquery/resources/admin/rest/UIProcessor.java
+++ b/backend/src/main/java/com/bakdata/conquery/resources/admin/rest/UIProcessor.java
@@ -68,8 +68,8 @@ public MetaStorage getStorage() {
return adminProcessor.getStorage();
}
- public UIContext getUIContext() {
- return new UIContext(adminProcessor.getNodeProvider());
+ public UIContext getUIContext(String csrfToken) {
+ return new UIContext(adminProcessor.getNodeProvider(), csrfToken);
}
public Set> getLoadedIndexes() {
diff --git a/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/AdminUIResource.java b/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/AdminUIResource.java
index c4fefb8b73..c503612d9c 100644
--- a/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/AdminUIResource.java
+++ b/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/AdminUIResource.java
@@ -4,6 +4,7 @@
import java.util.Objects;
import com.bakdata.conquery.models.auth.entities.Subject;
+import com.bakdata.conquery.models.auth.web.csrf.CsrfTokenSetFilter;
import com.bakdata.conquery.models.config.auth.AuthenticationConfig;
import com.bakdata.conquery.resources.ResourceConstants;
import com.bakdata.conquery.resources.admin.rest.UIProcessor;
@@ -27,34 +28,36 @@
public class AdminUIResource {
private final UIProcessor uiProcessor;
-
+ @Context
+ private ContainerRequestContext requestContext;
@GET
public View getIndex() {
- return new UIView<>("index.html.ftl", uiProcessor.getUIContext());
+ return new UIView<>("index.html.ftl", uiProcessor.getUIContext(CsrfTokenSetFilter.getCsrfTokenProperty(requestContext)));
}
@GET
@Path("script")
public View getScript() {
- return new UIView<>("script.html.ftl", uiProcessor.getUIContext());
+ return new UIView<>("script.html.ftl", uiProcessor.getUIContext(CsrfTokenSetFilter.getCsrfTokenProperty(requestContext)));
}
@GET
@Path("jobs")
public View getJobs() {
- return new UIView<>("jobs.html.ftl", uiProcessor.getUIContext(), uiProcessor.getAdminProcessor().getJobs());
+ return new UIView<>("jobs.html.ftl", uiProcessor.getUIContext(CsrfTokenSetFilter.getCsrfTokenProperty(requestContext)), uiProcessor.getAdminProcessor()
+ .getJobs());
}
@GET
@Path("queries")
public View getQueries() {
- return new UIView<>("queries.html.ftl", uiProcessor.getUIContext());
+ return new UIView<>("queries.html.ftl", uiProcessor.getUIContext(CsrfTokenSetFilter.getCsrfTokenProperty(requestContext)));
}
@GET
@Path("logout")
- public Response logout(@Context ContainerRequestContext requestContext, @Auth Subject user) {
+ public Response logout(@Auth Subject user) {
// Invalidate all cookies. At the moment the adminEnd uses cookies only for authentication, so this does not interfere with other things
final NewCookie[] expiredCookies = requestContext.getCookies().keySet().stream().map(AuthenticationConfig::expireCookie).toArray(NewCookie[]::new);
final URI logout = user.getAuthenticationInfo().getFrontChannelLogout();
diff --git a/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/AuthOverviewUIResource.java b/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/AuthOverviewUIResource.java
index f9ab29d6a5..969ec5d2c6 100644
--- a/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/AuthOverviewUIResource.java
+++ b/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/AuthOverviewUIResource.java
@@ -1,5 +1,6 @@
package com.bakdata.conquery.resources.admin.ui;
+import com.bakdata.conquery.models.auth.web.csrf.CsrfTokenSetFilter;
import com.bakdata.conquery.resources.ResourceConstants;
import com.bakdata.conquery.resources.admin.rest.UIProcessor;
import com.bakdata.conquery.resources.admin.ui.model.UIView;
@@ -8,6 +9,8 @@
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import lombok.RequiredArgsConstructor;
@@ -19,8 +22,8 @@ public class AuthOverviewUIResource {
protected final UIProcessor uiProcessor;
@GET
- public View getOverview() {
- return new UIView<>("authOverview.html.ftl", uiProcessor.getUIContext(), uiProcessor.getAuthOverview());
+ public View getOverview(@Context ContainerRequestContext request) {
+ return new UIView<>("authOverview.html.ftl", uiProcessor.getUIContext(CsrfTokenSetFilter.getCsrfTokenProperty(request)), uiProcessor.getAuthOverview());
}
}
diff --git a/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/ConceptsUIResource.java b/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/ConceptsUIResource.java
index fd7029ba17..ef1f88cecb 100644
--- a/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/ConceptsUIResource.java
+++ b/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/ConceptsUIResource.java
@@ -4,6 +4,7 @@
import static com.bakdata.conquery.resources.ResourceConstants.DATASET;
import com.bakdata.conquery.io.jersey.ExtraMimeTypes;
+import com.bakdata.conquery.models.auth.web.csrf.CsrfTokenSetFilter;
import com.bakdata.conquery.models.datasets.Dataset;
import com.bakdata.conquery.models.datasets.concepts.Concept;
import com.bakdata.conquery.resources.admin.rest.UIProcessor;
@@ -15,6 +16,8 @@
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@@ -35,12 +38,14 @@ public class ConceptsUIResource {
protected Concept> concept;
@PathParam(DATASET)
protected Dataset dataset;
+ @Context
+ ContainerRequestContext request;
@GET
public View getConceptView() {
return new UIView<>(
"concept.html.ftl",
- uiProcessor.getUIContext(),
+ uiProcessor.getUIContext(CsrfTokenSetFilter.getCsrfTokenProperty(request)),
concept
);
}
diff --git a/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/DatasetsUIResource.java b/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/DatasetsUIResource.java
index fef955311a..0e855e5786 100644
--- a/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/DatasetsUIResource.java
+++ b/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/DatasetsUIResource.java
@@ -6,6 +6,7 @@
import java.util.Collection;
import java.util.stream.Collectors;
+import com.bakdata.conquery.models.auth.web.csrf.CsrfTokenSetFilter;
import com.bakdata.conquery.models.datasets.Dataset;
import com.bakdata.conquery.models.datasets.Import;
import com.bakdata.conquery.models.datasets.SecondaryIdDescription;
@@ -16,6 +17,7 @@
import com.bakdata.conquery.models.index.search.SearchIndex;
import com.bakdata.conquery.models.worker.Namespace;
import com.bakdata.conquery.resources.admin.rest.UIProcessor;
+import com.bakdata.conquery.resources.admin.ui.model.UIContext;
import com.bakdata.conquery.resources.admin.ui.model.UIView;
import io.dropwizard.views.common.View;
import jakarta.inject.Inject;
@@ -23,6 +25,8 @@
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import lombok.AllArgsConstructor;
import lombok.Data;
@@ -45,13 +49,16 @@ public class DatasetsUIResource {
private final UIProcessor uiProcessor;
+ @Context
+ private ContainerRequestContext requestContext;
+
@GET
@Produces(MediaType.TEXT_HTML)
public View listDatasetsUI() {
return new UIView<>(
"datasets.html.ftl",
- uiProcessor.getUIContext(),
+ uiProcessor.getUIContext(CsrfTokenSetFilter.getCsrfTokenProperty(requestContext)),
uiProcessor.getDatasetRegistry().getAllDatasets()
);
}
@@ -63,7 +70,7 @@ public View getDataset(@PathParam(DATASET) Dataset dataset) {
final Namespace namespace = uiProcessor.getDatasetRegistry().get(dataset.getId());
return new UIView<>(
"dataset.html.ftl",
- uiProcessor.getUIContext(),
+ uiProcessor.getUIContext(CsrfTokenSetFilter.getCsrfTokenProperty(requestContext)),
new DatasetInfos(
namespace.getDataset(),
namespace.getStorage().getSecondaryIds(),
@@ -102,11 +109,13 @@ public View getDataset(@PathParam(DATASET) Dataset dataset) {
@Path("{" + DATASET + "}/mapping")
public View getIdMapping(@PathParam(DATASET) Dataset dataset) {
final Namespace namespace = uiProcessor.getDatasetRegistry().get(dataset.getId());
- EntityIdMap mapping = namespace.getStorage().getIdMapping();
+ final EntityIdMap mapping = namespace.getStorage().getIdMapping();
+ final UIContext uiContext = uiProcessor.getUIContext(CsrfTokenSetFilter.getCsrfTokenProperty(requestContext));
+
if (mapping != null && mapping.getInternalToPrint() != null) {
- return new UIView<>("idmapping.html.ftl", uiProcessor.getUIContext(), mapping.getInternalToPrint());
+ return new UIView<>("idmapping.html.ftl", uiContext, mapping.getInternalToPrint());
}
- return new UIView<>("add_idmapping.html.ftl", uiProcessor.getUIContext(), namespace.getDataset().getId());
+ return new UIView<>("add_idmapping.html.ftl", uiContext, namespace.getDataset().getId());
}
@Data
diff --git a/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/GroupUIResource.java b/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/GroupUIResource.java
index 7b4eb1e68d..c138afb3b6 100644
--- a/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/GroupUIResource.java
+++ b/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/GroupUIResource.java
@@ -1,20 +1,22 @@
package com.bakdata.conquery.resources.admin.ui;
+import static com.bakdata.conquery.resources.ResourceConstants.GROUPS_PATH_ELEMENT;
+import static com.bakdata.conquery.resources.ResourceConstants.GROUP_ID;
+
import com.bakdata.conquery.models.auth.entities.Group;
+import com.bakdata.conquery.models.auth.web.csrf.CsrfTokenSetFilter;
import com.bakdata.conquery.resources.admin.rest.UIProcessor;
import com.bakdata.conquery.resources.admin.ui.model.UIView;
import io.dropwizard.views.common.View;
-import lombok.RequiredArgsConstructor;
-
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
-
-import static com.bakdata.conquery.resources.ResourceConstants.GROUPS_PATH_ELEMENT;
-import static com.bakdata.conquery.resources.ResourceConstants.GROUP_ID;
+import lombok.RequiredArgsConstructor;
@Produces(MediaType.TEXT_HTML)
@Path(GROUPS_PATH_ELEMENT)
@@ -22,10 +24,13 @@
public class GroupUIResource {
protected final UIProcessor uiProcessor;
+ @Context
+ private ContainerRequestContext requestContext;
@GET
public View getGroups() {
- return new UIView<>("groups.html.ftl", uiProcessor.getUIContext(), uiProcessor.getAdminProcessor().getAllGroups());
+ return new UIView<>("groups.html.ftl", uiProcessor.getUIContext(CsrfTokenSetFilter.getCsrfTokenProperty(requestContext)), uiProcessor.getAdminProcessor()
+ .getAllGroups());
}
/**
@@ -37,6 +42,6 @@ public View getGroups() {
@Path("{" + GROUP_ID + "}")
@GET
public View getGroup(@PathParam(GROUP_ID) Group group) {
- return new UIView<>("group.html.ftl", uiProcessor.getUIContext(), uiProcessor.getGroupContent(group));
+ return new UIView<>("group.html.ftl", uiProcessor.getUIContext(CsrfTokenSetFilter.getCsrfTokenProperty(requestContext)), uiProcessor.getGroupContent(group));
}
}
\ No newline at end of file
diff --git a/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/IndexServiceUIResource.java b/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/IndexServiceUIResource.java
index 557b8e1dec..dd1ee509fd 100644
--- a/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/IndexServiceUIResource.java
+++ b/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/IndexServiceUIResource.java
@@ -4,17 +4,19 @@
import java.util.Set;
-import jakarta.inject.Inject;
-import jakarta.ws.rs.GET;
-import jakarta.ws.rs.Path;
-import jakarta.ws.rs.Produces;
-import jakarta.ws.rs.core.MediaType;
-
+import com.bakdata.conquery.models.auth.web.csrf.CsrfTokenSetFilter;
import com.bakdata.conquery.models.index.IndexKey;
import com.bakdata.conquery.resources.admin.rest.UIProcessor;
import com.bakdata.conquery.resources.admin.ui.model.UIView;
import com.google.common.cache.CacheStats;
import io.dropwizard.views.common.View;
+import jakarta.inject.Inject;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.MediaType;
import lombok.Data;
import lombok.RequiredArgsConstructor;
@@ -24,12 +26,14 @@
public class IndexServiceUIResource {
private final UIProcessor uiProcessor;
+ @Context
+ private ContainerRequestContext requestContext;
@GET
@Path(INDEX_SERVICE_PATH_ELEMENT)
public View getIndexService() {
final IndexServiceUIContent content = new IndexServiceUIContent(uiProcessor.getIndexServiceStatistics(), uiProcessor.getLoadedIndexes());
- return new UIView<>("indexService.html.ftl", uiProcessor.getUIContext(), content);
+ return new UIView<>("indexService.html.ftl", uiProcessor.getUIContext(CsrfTokenSetFilter.getCsrfTokenProperty(requestContext)), content);
}
/**
diff --git a/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/RoleUIResource.java b/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/RoleUIResource.java
index ff7ebfc8e4..dce61a5219 100644
--- a/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/RoleUIResource.java
+++ b/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/RoleUIResource.java
@@ -4,6 +4,7 @@
import static com.bakdata.conquery.resources.ResourceConstants.ROLE_ID;
import com.bakdata.conquery.models.auth.entities.Role;
+import com.bakdata.conquery.models.auth.web.csrf.CsrfTokenSetFilter;
import com.bakdata.conquery.resources.admin.rest.UIProcessor;
import com.bakdata.conquery.resources.admin.ui.model.UIView;
import io.dropwizard.views.common.View;
@@ -12,6 +13,8 @@
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import lombok.RequiredArgsConstructor;
@@ -21,10 +24,13 @@
public class RoleUIResource {
protected final UIProcessor uiProcessor;
+ @Context
+ private ContainerRequestContext requestContext;
@GET
public View getRoles() {
- return new UIView<>("roles.html.ftl", uiProcessor.getUIContext(), uiProcessor.getAdminProcessor().getAllRoles());
+ return new UIView<>("roles.html.ftl", uiProcessor.getUIContext(CsrfTokenSetFilter.getCsrfTokenProperty(requestContext)), uiProcessor.getAdminProcessor()
+ .getAllRoles());
}
/**
@@ -37,6 +43,6 @@ public View getRoles() {
@Path("{" + ROLE_ID + "}")
@GET
public View getRole(@PathParam(ROLE_ID) Role role) {
- return new UIView<>("role.html.ftl", uiProcessor.getUIContext(), uiProcessor.getRoleContent(role));
+ return new UIView<>("role.html.ftl", uiProcessor.getUIContext(CsrfTokenSetFilter.getCsrfTokenProperty(requestContext)), uiProcessor.getRoleContent(role));
}
}
diff --git a/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/TablesUIResource.java b/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/TablesUIResource.java
index 1aa4fbe0d8..2ede9a049c 100644
--- a/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/TablesUIResource.java
+++ b/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/TablesUIResource.java
@@ -2,6 +2,7 @@
import static com.bakdata.conquery.resources.ResourceConstants.*;
+import com.bakdata.conquery.models.auth.web.csrf.CsrfTokenSetFilter;
import com.bakdata.conquery.models.datasets.Dataset;
import com.bakdata.conquery.models.datasets.Import;
import com.bakdata.conquery.models.datasets.Table;
@@ -13,6 +14,8 @@
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@@ -33,13 +36,15 @@ public class TablesUIResource {
private Dataset dataset;
@PathParam(TABLE)
private Table table;
+ @Context
+ private ContainerRequestContext requestContext;
@GET
public View getTableView() {
return new UIView<>(
"table.html.ftl",
- uiProcessor.getUIContext(),
+ uiProcessor.getUIContext(CsrfTokenSetFilter.getCsrfTokenProperty(requestContext)),
uiProcessor.getTableStatistics(table)
);
}
@@ -50,7 +55,7 @@ public View getImportView(@PathParam(IMPORT_ID) Import imp) {
return new UIView<>(
"import.html.ftl",
- uiProcessor.getUIContext(),
+ uiProcessor.getUIContext(CsrfTokenSetFilter.getCsrfTokenProperty(requestContext)),
uiProcessor.getImportStatistics(imp)
);
}
diff --git a/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/UserUIResource.java b/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/UserUIResource.java
index 892528a8d1..e8833a6b74 100644
--- a/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/UserUIResource.java
+++ b/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/UserUIResource.java
@@ -4,6 +4,7 @@
import static com.bakdata.conquery.resources.ResourceConstants.USER_ID;
import com.bakdata.conquery.models.auth.entities.User;
+import com.bakdata.conquery.models.auth.web.csrf.CsrfTokenSetFilter;
import com.bakdata.conquery.resources.admin.rest.UIProcessor;
import com.bakdata.conquery.resources.admin.ui.model.UIView;
import io.dropwizard.views.common.View;
@@ -12,6 +13,8 @@
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import lombok.RequiredArgsConstructor;
@@ -21,10 +24,13 @@
public class UserUIResource {
protected final UIProcessor uiProcessor;
+ @Context
+ private ContainerRequestContext requestContext;
@GET
public View getUsers() {
- return new UIView<>("users.html.ftl", uiProcessor.getUIContext(), uiProcessor.getAdminProcessor().getAllUsers());
+ return new UIView<>("users.html.ftl", uiProcessor.getUIContext(CsrfTokenSetFilter.getCsrfTokenProperty(requestContext)), uiProcessor.getAdminProcessor()
+ .getAllUsers());
}
/**
@@ -36,6 +42,6 @@ public View getUsers() {
@Path("{" + USER_ID + "}")
@GET
public View getUser(@PathParam(USER_ID) User user) {
- return new UIView<>("user.html.ftl", uiProcessor.getUIContext(), uiProcessor.getUserContent(user));
+ return new UIView<>("user.html.ftl", uiProcessor.getUIContext(CsrfTokenSetFilter.getCsrfTokenProperty(requestContext)), uiProcessor.getUserContent(user));
}
}
diff --git a/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/model/ConnectorUIResource.java b/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/model/ConnectorUIResource.java
index 4168fa7ab2..c846a92b30 100644
--- a/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/model/ConnectorUIResource.java
+++ b/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/model/ConnectorUIResource.java
@@ -4,6 +4,7 @@
import static com.bakdata.conquery.resources.ResourceConstants.DATASET;
import com.bakdata.conquery.io.jersey.ExtraMimeTypes;
+import com.bakdata.conquery.models.auth.web.csrf.CsrfTokenSetFilter;
import com.bakdata.conquery.models.datasets.Dataset;
import com.bakdata.conquery.models.datasets.concepts.Connector;
import com.bakdata.conquery.resources.admin.rest.UIProcessor;
@@ -14,6 +15,8 @@
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@@ -34,12 +37,14 @@ public class ConnectorUIResource {
protected Connector connector;
@PathParam(DATASET)
protected Dataset dataset;
+ @Context
+ private ContainerRequestContext requestContext;
@GET
public View getConnectorView() {
return new UIView<>(
"connector.html.ftl",
- uiProcessor.getUIContext(),
+ uiProcessor.getUIContext(CsrfTokenSetFilter.getCsrfTokenProperty(requestContext)),
connector
);
}
diff --git a/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/model/UIContext.java b/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/model/UIContext.java
index 83d48c9271..ef6fe0ccd7 100644
--- a/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/model/UIContext.java
+++ b/backend/src/main/java/com/bakdata/conquery/resources/admin/ui/model/UIContext.java
@@ -23,6 +23,9 @@ public class UIContext {
@Getter
public final TemplateModel staticUriElem = STATIC_URI_ELEMENTS;
+ @Getter
+ public final String csrfToken;
+
public Map getShardNodes() {
return shardNodeSupplier.get().stream().collect(Collectors.toMap(
ShardNodeInformation::getRemoteAddress,
diff --git a/backend/src/main/resources/assets/custom/js/script.js b/backend/src/main/resources/assets/custom/js/script.js
index 5e12ff0b29..b87347a72f 100644
--- a/backend/src/main/resources/assets/custom/js/script.js
+++ b/backend/src/main/resources/assets/custom/js/script.js
@@ -30,7 +30,9 @@ async function rest(url, options) {
method: 'get',
credentials: 'same-origin',
headers: {
- 'Content-Type': 'application/json'
+ 'Content-Type': 'application/json',
+ // Csrf token is provided via global variable
+ 'X-Csrf-Token': csrf_token
},
...options
}
@@ -38,6 +40,19 @@ async function rest(url, options) {
return res;
}
+async function restOptionalForce(url, options) {
+ return rest(url, options).then((res) => {
+ // force button in case of 409 status
+ const customButton = createCustomButton('Force delete');
+ customButton.onclick = () => rest(toForceURL(url), options).then((res) => {
+ res.ok && location.reload();
+ });
+
+ showMessageForResponse(res, customButton);
+ return res;
+ });
+}
+
function getToast(type, title, text, smalltext = "", customButton) {
if (!type) type = ToastTypes.INFO;
@@ -207,4 +222,71 @@ $("ul.nav-tabs > li > a").on("shown.bs.tab", function (e) {
// on load of the page: switch to the currently selected tab
var hash = window.location.hash;
-$('#myTab a[href="' + hash + '"]').tab('show');
\ No newline at end of file
+$('#myTab a[href="' + hash + '"]').tab('show');
+
+const uploadFormMapping = {
+ mapping: {
+ name: "mapping",
+ uri: "internToExtern",
+ accept: "*.mapping.json",
+ },
+ table: { name: "table_schema", uri: "tables", accept: "*.table.json" },
+ concept: {
+ name: "concept_schema",
+ uri: "concepts",
+ accept: "*.concept.json",
+ },
+ structure: {
+ name: "structure_schema",
+ uri: "structure",
+ accept: "structure.json",
+ },
+};
+
+function updateDatasetUploadForm(select) {
+ const data = uploadFormMapping[select.value];
+ const fileInput = $(select).next();
+ fileInput.value = "";
+ fileInput.attr("accept", data.accept);
+ fileInput.attr("name", data.name);
+ $(select)
+ .parent()
+ .attr(
+ "onsubmit",
+ "postFile(event, '/admin/datasets/${c.ds.id}/" + data.uri + "')"
+ );
+}
+
+function createDataset(event) {
+ event.preventDefault();
+ rest(
+ '/admin/datasets',
+ {
+ method: 'post',
+ body: JSON.stringify({
+ name: document.getElementById('entity_id').value,
+ label: document.getElementById('entity_name').value
+ })
+ }).then(function (res) {
+ if (res.ok)
+ location.reload();
+ else
+ showMessageForResponse(res);
+ });
+}
+
+
+
+function deleteDataset(event, datasetId) {
+ event.preventDefault();
+ rest(
+ `/admin/datasets/${datasetId}`,
+ {
+ method: 'delete',
+ }).then(function (res) {
+ if (res.ok)
+ location.reload();
+ else
+ showMessageForResponse(res);
+ });
+}
\ No newline at end of file
diff --git a/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/dataset.html.ftl b/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/dataset.html.ftl
index 51b1aa8e22..3010d20c54 100644
--- a/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/dataset.html.ftl
+++ b/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/dataset.html.ftl
@@ -27,8 +27,6 @@
<#macro idMapping>Here#macro>
<@layout.layout>
-
-
<@breadcrumbs.breadcrumbs
diff --git a/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/datasets.html.ftl b/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/datasets.html.ftl
index 353adce52b..ae11931ea6 100644
--- a/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/datasets.html.ftl
+++ b/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/datasets.html.ftl
@@ -3,7 +3,7 @@
<#assign columns=["id", "label", "actions"]>
<#macro deleteDatasetButton id>
-
+
#macro>
<@layout.layout>
@@ -21,7 +21,7 @@
-
+
@@ -32,43 +32,4 @@
-
@layout.layout>
\ No newline at end of file
diff --git a/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/scripts/dataset.js b/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/scripts/dataset.js
deleted file mode 100644
index 6cbb9c846e..0000000000
--- a/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/scripts/dataset.js
+++ /dev/null
@@ -1,45 +0,0 @@
-const uploadFormMapping = {
- mapping: {
- name: "mapping",
- uri: "internToExtern",
- accept: "*.mapping.json",
- },
- table: { name: "table_schema", uri: "tables", accept: "*.table.json" },
- concept: {
- name: "concept_schema",
- uri: "concepts",
- accept: "*.concept.json",
- },
- structure: {
- name: "structure_schema",
- uri: "structure",
- accept: "structure.json",
- },
-};
-
-function updateDatasetUploadForm(select) {
- const data = uploadFormMapping[select.value];
- const fileInput = $(select).next();
- fileInput.value = "";
- fileInput.attr("accept", data.accept);
- fileInput.attr("name", data.name);
- $(select)
- .parent()
- .attr(
- "onsubmit",
- "postFile(event, '/admin/datasets/${c.ds.id}/" + data.uri + "')"
- );
-}
-
-async function restOptionalForce(url, options) {
- return rest(url, options).then((res) => {
- // force button in case of 409 status
- const customButton = createCustomButton('Force delete');
- customButton.onclick = () => rest(toForceURL(url), options).then((res) => {
- res.ok && location.reload();
- });
-
- showMessageForResponse(res, customButton);
- return res;
- });
-}
\ No newline at end of file
diff --git a/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/templates/base.html.ftl b/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/templates/base.html.ftl
index e8e5a180b4..fc5435567b 100644
--- a/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/templates/base.html.ftl
+++ b/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/templates/base.html.ftl
@@ -1,4 +1,4 @@
-<#macro html title>
+<#macro html title >
@@ -16,6 +16,13 @@
+
+
+
+
${title}
<#nested />
From cdcb67b7ac637cba02152831192286cc319e0954 Mon Sep 17 00:00:00 2001
From: Max Thonagel <12283268+thoniTUB@users.noreply.github.com>
Date: Thu, 25 Apr 2024 21:13:40 +0200
Subject: [PATCH 02/11] fixes admin ui templates
Signed-off-by: Max Thonagel <12283268+thoniTUB@users.noreply.github.com>
---
.../auth/web/csrf/CsrfTokenSetFilter.java | 2 +-
.../main/resources/assets/custom/js/script.js | 17 ++++++++++-------
.../resources/admin/ui/dataset.html.ftl | 2 +-
3 files changed, 12 insertions(+), 9 deletions(-)
diff --git a/backend/src/main/java/com/bakdata/conquery/models/auth/web/csrf/CsrfTokenSetFilter.java b/backend/src/main/java/com/bakdata/conquery/models/auth/web/csrf/CsrfTokenSetFilter.java
index a0e7f47047..bd01af11f4 100644
--- a/backend/src/main/java/com/bakdata/conquery/models/auth/web/csrf/CsrfTokenSetFilter.java
+++ b/backend/src/main/java/com/bakdata/conquery/models/auth/web/csrf/CsrfTokenSetFilter.java
@@ -27,7 +27,7 @@ public class CsrfTokenSetFilter implements ContainerRequestFilter, ContainerResp
public static final String CSRF_TOKEN_PROPERTY = "csrf_token";
public static final int TOKEN_LENGTH = 30;
- Random random = new SecureRandom();
+ private final Random random = new SecureRandom();
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
diff --git a/backend/src/main/resources/assets/custom/js/script.js b/backend/src/main/resources/assets/custom/js/script.js
index b87347a72f..c574f0fe4c 100644
--- a/backend/src/main/resources/assets/custom/js/script.js
+++ b/backend/src/main/resources/assets/custom/js/script.js
@@ -163,10 +163,9 @@ function postFile(event, url) {
let reader = new FileReader();
reader.onload = function () {
let json = reader.result;
- fetch(url, {
- method: 'post', credentials: 'same-origin', body: json, headers: {
- "Content-Type": "application/json"
- }
+ rest(url, {
+ method: 'post',
+ body: json
})
.then(function (response) {
if (response.ok) {
@@ -230,7 +229,11 @@ const uploadFormMapping = {
uri: "internToExtern",
accept: "*.mapping.json",
},
- table: { name: "table_schema", uri: "tables", accept: "*.table.json" },
+ table: {
+ name: "table_schema",
+ uri: "tables",
+ accept: "*.table.json"
+ },
concept: {
name: "concept_schema",
uri: "concepts",
@@ -243,7 +246,7 @@ const uploadFormMapping = {
},
};
-function updateDatasetUploadForm(select) {
+function updateDatasetUploadForm(select, datasetId) {
const data = uploadFormMapping[select.value];
const fileInput = $(select).next();
fileInput.value = "";
@@ -253,7 +256,7 @@ function updateDatasetUploadForm(select) {
.parent()
.attr(
"onsubmit",
- "postFile(event, '/admin/datasets/${c.ds.id}/" + data.uri + "')"
+ `postFile(event, '/admin/datasets/${datasetId}/${data.uri}')`
);
}
diff --git a/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/dataset.html.ftl b/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/dataset.html.ftl
index 3010d20c54..65d07c7f6d 100644
--- a/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/dataset.html.ftl
+++ b/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/dataset.html.ftl
@@ -49,7 +49,7 @@