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. + * + */ +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 <@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> - + <@layout.layout> @@ -21,7 +21,7 @@ - + @@ -32,43 +32,4 @@ - \ 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 @@ Update automatically.
@@ -230,7 +200,7 @@
@@ -244,7 +214,7 @@
@@ -258,7 +228,7 @@
@@ -272,7 +242,7 @@
@@ -283,5 +253,4 @@
- - \ No newline at end of file + \ No newline at end of file diff --git a/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/templates/authEntityOverview.html.ftl b/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/templates/authEntityOverview.html.ftl index 371a846874..dd62fa7206 100644 --- a/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/templates/authEntityOverview.html.ftl +++ b/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/templates/authEntityOverview.html.ftl @@ -46,11 +46,9 @@