diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 570eb425f6..7d8fe7ddd0 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -44,10 +44,12 @@ jobs: - name: Checkout repository uses: actions/checkout@v2 - - name: set java 17 - uses: actions/setup-java@v1 + - name: Set up JDK + uses: actions/setup-java@v4 with: - java-version: 17 + distribution: 'temurin' + java-version: '17' + overwrite-settings: false # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/test_cypress.yml b/.github/workflows/test_cypress.yml index cabccfb095..8990da08ab 100644 --- a/.github/workflows/test_cypress.yml +++ b/.github/workflows/test_cypress.yml @@ -17,26 +17,28 @@ jobs: timeout-minutes: 10 steps: - name: Cache local Maven repository - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} restore-keys: | ${{ runner.os }}-maven- - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: true - name: Setup Node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 18 - name: Set up JDK - uses: actions/setup-java@v1 + uses: actions/setup-java@v4 with: - java-version: 17 + distribution: 'temurin' + java-version: '17' + overwrite-settings: false - name: Build Backend run: ./scripts/build_backend_no_version.sh @@ -53,14 +55,16 @@ jobs: start: bash ./scripts/run_e2e_all.sh wait-on: "http://localhost:8000" - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: failure() with: name: cypress-screenshots path: cypress/screenshots + if-no-files-found: ignore - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: failure() with: name: cypress-videos path: cypress/videos + if-no-files-found: ignore diff --git a/backend/src/main/java/com/bakdata/conquery/apiv1/ApiV1.java b/backend/src/main/java/com/bakdata/conquery/apiv1/ApiV1.java index a9cfc96914..ffc6ef6f4a 100644 --- a/backend/src/main/java/com/bakdata/conquery/apiv1/ApiV1.java +++ b/backend/src/main/java/com/bakdata/conquery/apiv1/ApiV1.java @@ -9,7 +9,7 @@ import com.bakdata.conquery.io.result.ResultRender.ResultRendererProvider; import com.bakdata.conquery.metrics.ActiveUsersFilter; import com.bakdata.conquery.models.auth.basic.JWTokenHandler; -import com.bakdata.conquery.models.auth.web.DefaultAuthFilter; +import com.bakdata.conquery.models.auth.web.AuthFilter; import com.bakdata.conquery.models.forms.frontendconfiguration.FormConfigProcessor; import com.bakdata.conquery.models.forms.frontendconfiguration.FormProcessor; import com.bakdata.conquery.resources.ResourcesProvider; @@ -63,8 +63,8 @@ protected void configure() { * We use the same instance of the filter for the api servlet and the admin servlet to have a single * point for authentication. */ - jersey.register(DefaultAuthFilter.class); - DefaultAuthFilter.registerTokenExtractor(JWTokenHandler.JWTokenExtractor.class, jersey.getResourceConfig()); + jersey.register(AuthFilter.class); + AuthFilter.registerTokenExtractor(JWTokenHandler.JWTokenExtractor.class, jersey.getResourceConfig()); jersey.register(IdParamConverter.Provider.INSTANCE); diff --git a/backend/src/main/java/com/bakdata/conquery/models/auth/AuthorizationController.java b/backend/src/main/java/com/bakdata/conquery/models/auth/AuthorizationController.java index f36044bb6b..a6490c4b73 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/auth/AuthorizationController.java +++ b/backend/src/main/java/com/bakdata/conquery/models/auth/AuthorizationController.java @@ -15,7 +15,7 @@ import com.bakdata.conquery.models.auth.entities.User; import com.bakdata.conquery.models.auth.permissions.Ability; import com.bakdata.conquery.models.auth.permissions.ConqueryPermission; -import com.bakdata.conquery.models.auth.web.DefaultAuthFilter; +import com.bakdata.conquery.models.auth.web.AuthFilter; import com.bakdata.conquery.models.auth.web.RedirectingAuthFilter; import com.bakdata.conquery.models.config.ConqueryConfig; import com.bakdata.conquery.models.config.auth.AuthenticationRealmFactory; @@ -85,18 +85,18 @@ public AuthorizationController(MetaStorage storage, ConqueryConfig config, Envir this.adminServlet = adminServlet; if (adminServlet != null) { - adminServlet.getJerseyConfig().register(DefaultAuthFilter.class); - DefaultAuthFilter.registerTokenExtractor(JWTokenHandler.JWTokenExtractor.class, adminServlet.getJerseyConfig()); + adminServlet.getJerseyConfig().register(AuthFilter.class); + AuthFilter.registerTokenExtractor(JWTokenHandler.JWTokenExtractor.class, adminServlet.getJerseyConfig()); // The binding is necessary here because the RedirectingAuthFitler delegates to the DefaultAuthfilter at the moment adminServlet.getJerseyConfigUI().register(new AbstractBinder() { @Override protected void configure() { - bindAsContract(DefaultAuthFilter.class); + bindAsContract(AuthFilter.class); } }); adminServlet.getJerseyConfigUI().register(RedirectingAuthFilter.class); - DefaultAuthFilter.registerTokenExtractor(JWTokenHandler.JWTokenExtractor.class, adminServlet.getJerseyConfigUI()); + AuthFilter.registerTokenExtractor(JWTokenHandler.JWTokenExtractor.class, adminServlet.getJerseyConfigUI()); } unprotectedAuthAdmin = AuthServlet.generalSetup(environment.metrics(), config, environment.admin(), environment.getObjectMapper()); diff --git a/backend/src/main/java/com/bakdata/conquery/models/auth/basic/JWTokenHandler.java b/backend/src/main/java/com/bakdata/conquery/models/auth/basic/JWTokenHandler.java index e29fbc5df6..04930ca201 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/auth/basic/JWTokenHandler.java +++ b/backend/src/main/java/com/bakdata/conquery/models/auth/basic/JWTokenHandler.java @@ -9,10 +9,9 @@ import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTDecodeException; -import com.bakdata.conquery.models.auth.web.DefaultAuthFilter; +import com.bakdata.conquery.models.auth.web.AuthFilter; import io.dropwizard.auth.oauth.OAuthCredentialAuthFilter; import io.dropwizard.util.Duration; -import jakarta.inject.Named; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.core.HttpHeaders; import lombok.experimental.UtilityClass; @@ -21,7 +20,6 @@ import org.apache.commons.lang3.time.DateUtils; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.BearerToken; -import org.jvnet.hk2.annotations.Service; @UtilityClass @Slf4j @@ -100,7 +98,7 @@ public static String generateTokenSecret() { * @param request * @return */ - public static class JWTokenExtractor implements DefaultAuthFilter.TokenExtractor { + public static class JWTokenExtractor implements AuthFilter.TokenExtractor { @Override public AuthenticationToken apply(ContainerRequestContext request) { diff --git a/backend/src/main/java/com/bakdata/conquery/models/auth/develop/DevAuthConfig.java b/backend/src/main/java/com/bakdata/conquery/models/auth/develop/DevAuthConfig.java index cb887b9b9f..d28e350c44 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/auth/develop/DevAuthConfig.java +++ b/backend/src/main/java/com/bakdata/conquery/models/auth/develop/DevAuthConfig.java @@ -3,7 +3,7 @@ import com.bakdata.conquery.io.cps.CPSType; import com.bakdata.conquery.models.auth.AuthorizationController; import com.bakdata.conquery.models.auth.ConqueryAuthenticationRealm; -import com.bakdata.conquery.models.auth.web.DefaultAuthFilter; +import com.bakdata.conquery.models.auth.web.AuthFilter; import com.bakdata.conquery.models.config.ConqueryConfig; import com.bakdata.conquery.models.config.auth.AuthenticationRealmFactory; import com.bakdata.conquery.models.identifiable.ids.specific.UserId; @@ -19,10 +19,10 @@ public class DevAuthConfig implements AuthenticationRealmFactory { @Override public ConqueryAuthenticationRealm createRealm(Environment environment, ConqueryConfig config, AuthorizationController authorizationController) { - DefaultAuthFilter.registerTokenExtractor(UserIdTokenExtractor.class, environment.jersey().getResourceConfig()); + AuthFilter.registerTokenExtractor(UserIdTokenExtractor.class, environment.jersey().getResourceConfig()); if (authorizationController.getAdminServlet() != null) { - DefaultAuthFilter.registerTokenExtractor(UserIdTokenExtractor.class, authorizationController.getAdminServlet().getJerseyConfig()); - DefaultAuthFilter.registerTokenExtractor(UserIdTokenExtractor.class, authorizationController.getAdminServlet().getJerseyConfigUI()); + AuthFilter.registerTokenExtractor(UserIdTokenExtractor.class, authorizationController.getAdminServlet().getJerseyConfig()); + AuthFilter.registerTokenExtractor(UserIdTokenExtractor.class, authorizationController.getAdminServlet().getJerseyConfigUI()); } // Use the first defined user als the default user. This is usually the superuser if the DevelopmentAuthorizationConfig is set diff --git a/backend/src/main/java/com/bakdata/conquery/models/auth/develop/UserIdTokenExtractor.java b/backend/src/main/java/com/bakdata/conquery/models/auth/develop/UserIdTokenExtractor.java index 3c5f513e51..cd906eac6c 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/auth/develop/UserIdTokenExtractor.java +++ b/backend/src/main/java/com/bakdata/conquery/models/auth/develop/UserIdTokenExtractor.java @@ -1,6 +1,6 @@ package com.bakdata.conquery.models.auth.develop; -import com.bakdata.conquery.models.auth.web.DefaultAuthFilter; +import com.bakdata.conquery.models.auth.web.AuthFilter; import com.bakdata.conquery.models.identifiable.ids.specific.UserId; import jakarta.inject.Named; import jakarta.ws.rs.container.ContainerRequestContext; @@ -15,7 +15,7 @@ @RequiredArgsConstructor @Service @Named("user-id") -public class UserIdTokenExtractor implements DefaultAuthFilter.TokenExtractor { +public class UserIdTokenExtractor implements AuthFilter.TokenExtractor { private static final String UID_QUERY_STRING_PARAMETER = "access_token"; diff --git a/backend/src/main/java/com/bakdata/conquery/models/auth/permissions/StringPermissionBuilder.java b/backend/src/main/java/com/bakdata/conquery/models/auth/permissions/StringPermissionBuilder.java index a906d4514d..09b53ba686 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/auth/permissions/StringPermissionBuilder.java +++ b/backend/src/main/java/com/bakdata/conquery/models/auth/permissions/StringPermissionBuilder.java @@ -1,17 +1,17 @@ package com.bakdata.conquery.models.auth.permissions; -import com.bakdata.conquery.io.cps.CPSBase; -import com.bakdata.conquery.resources.admin.rest.AdminProcessor; - import java.util.Set; import java.util.stream.Collectors; +import com.bakdata.conquery.io.cps.CPSBase; +import com.bakdata.conquery.resources.admin.rest.UIProcessor; + /** * Base class with utility functions to build permissions. * Subclasses should wrap their call to the {@code instancePermission} functions with their own string representation. - * {@link com.bakdata.conquery.io.cps.CPSType} is used by {@link AdminProcessor#preparePermissionTemplate()} to generate + * {@link com.bakdata.conquery.io.cps.CPSType} is used by {@link UIProcessor#preparePermissionTemplate()} to generate * a view ob possible Permissions for creation. - * Therefore every builder should have a static INSTANCE-Field. + * Therefore, every builder should have a static INSTANCE-Field. */ @CPSBase public abstract class StringPermissionBuilder { 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..25ab71e036 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"; @@ -42,7 +46,7 @@ public class AuthCookieFilter implements ContainerRequestFilter, ContainerRespon /** * The filter tries to extract a token from a cookie and puts it into the - * authorization header of the request. This simplifies the token retrival + * authorization header of the request. This simplifies the token retrieval * process for the realms. */ @Override diff --git a/backend/src/main/java/com/bakdata/conquery/models/auth/web/DefaultAuthFilter.java b/backend/src/main/java/com/bakdata/conquery/models/auth/web/AuthFilter.java similarity index 89% rename from backend/src/main/java/com/bakdata/conquery/models/auth/web/DefaultAuthFilter.java rename to backend/src/main/java/com/bakdata/conquery/models/auth/web/AuthFilter.java index a1793ebb27..24a0273966 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/auth/web/DefaultAuthFilter.java +++ b/backend/src/main/java/com/bakdata/conquery/models/auth/web/AuthFilter.java @@ -8,7 +8,6 @@ import com.bakdata.conquery.models.auth.ConqueryAuthenticator; import com.bakdata.conquery.models.auth.entities.Subject; import com.google.common.base.Function; -import io.dropwizard.auth.AuthFilter; import io.dropwizard.auth.DefaultUnauthorizedHandler; import jakarta.annotation.Priority; import jakarta.inject.Inject; @@ -17,9 +16,6 @@ import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.PreMatching; import jakarta.ws.rs.core.SecurityContext; -import jakarta.ws.rs.ext.Provider; -import lombok.RequiredArgsConstructor; -import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationToken; @@ -32,17 +28,19 @@ * security relevant information for protected resources. The request is first * submitted to the registered {@link ConqueryAuthenticationRealm}s for the * token extraction, then the extracted tokens are submitted these realms - * through Dropwizards {@link AuthFilter} and Shiro. + * through Dropwizards {@link io.dropwizard.auth.AuthFilter} and Shiro. */ @Slf4j @PreMatching -@Priority(Priorities.AUTHENTICATION) -public class DefaultAuthFilter extends AuthFilter { +@Priority(AuthFilter.PRIORITY) +public class AuthFilter extends io.dropwizard.auth.AuthFilter { + + public static final int PRIORITY = Priorities.AUTHENTICATION; private final IterableProvider tokenExtractors; @Inject - public DefaultAuthFilter(IterableProvider tokenExtractors) { + public AuthFilter(IterableProvider tokenExtractors) { this.tokenExtractors = tokenExtractors; this.authenticator = new ConqueryAuthenticator(); this.unauthorizedHandler = new DefaultUnauthorizedHandler(); @@ -102,7 +100,7 @@ public static void registerTokenExtractor(Class extrac * Authenticating realms need to be able to extract a token from a request. How * it performs the extraction is implementation dependent. Anyway the realm * should NOT alter the request. This function is called prior to the - * authentication process in the {@link DefaultAuthFilter}. After the token + * authentication process in the {@link AuthFilter}. After the token * extraction process the Token is resubmitted to the realm from the AuthFilter * to the {@link ConqueryAuthenticator} which dispatches it to shiro. * diff --git a/backend/src/main/java/com/bakdata/conquery/models/auth/web/RedirectingAuthFilter.java b/backend/src/main/java/com/bakdata/conquery/models/auth/web/RedirectingAuthFilter.java index c1ed9cca47..4d4a214bfd 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/auth/web/RedirectingAuthFilter.java +++ b/backend/src/main/java/com/bakdata/conquery/models/auth/web/RedirectingAuthFilter.java @@ -8,7 +8,6 @@ import com.bakdata.conquery.models.auth.entities.User; import com.bakdata.conquery.resources.admin.ui.model.UIView; -import io.dropwizard.auth.AuthFilter; import jakarta.annotation.Priority; import jakarta.inject.Inject; import jakarta.ws.rs.BadRequestException; @@ -40,10 +39,12 @@ * the login schema can be chosen. */ @Slf4j -@Priority(Priorities.AUTHENTICATION - 1) +@Priority(RedirectingAuthFilter.PRIORITY) @PreMatching @RequiredArgsConstructor(onConstructor_ = {@Inject}) -public class RedirectingAuthFilter extends AuthFilter { +public class RedirectingAuthFilter extends io.dropwizard.auth.AuthFilter { + + public static final int PRIORITY = Priorities.AUTHENTICATION; public static final String REDIRECT_URI = "redirect_uri"; /** @@ -61,7 +62,7 @@ public class RedirectingAuthFilter extends AuthFilter /** * The Filter that checks if a request was authenticated */ - private final DefaultAuthFilter delegate; + private final AuthFilter delegate; public static void registerLoginInitiator(ResourceConfig resourceConfig, LoginInitiator initiator, final String name) { resourceConfig.register(new AbstractBinder() { 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..910af59848 --- /dev/null +++ b/backend/src/main/java/com/bakdata/conquery/models/auth/web/csrf/CsrfTokenCheckFilter.java @@ -0,0 +1,58 @@ +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.HttpMethod; +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 + cookieTokenHash = + Optional.ofNullable(requestContext.getCookies().get(CsrfTokenSetFilter.CSRF_COOKIE_NAME)).map(Cookie::getValue).orElse(null); + final String headerToken = requestContext.getHeaders().getFirst(CSRF_TOKEN_HEADER); + + final String method = requestContext.getMethod(); + + if (HttpMethod.GET.equals(method) || HttpMethod.HEAD.equals(method) || HttpMethod.OPTIONS.equals(method)) { + log.trace("Skipping csrf check because request is not state changing (method={})", method); + return; + } + + if (cookieTokenHash == 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 (!CsrfTokenSetFilter.checkHash(headerToken, cookieTokenHash)) { + log.warn("Request csrf cookie and header did not match"); + log.trace("header-token={} cookie-token-hash={}", headerToken, cookieTokenHash); + 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..939db8a213 --- /dev/null +++ b/backend/src/main/java/com/bakdata/conquery/models/auth/web/csrf/CsrfTokenSetFilter.java @@ -0,0 +1,122 @@ +package com.bakdata.conquery.models.auth.web.csrf; + +import java.io.IOException; +import java.security.SecureRandom; +import java.util.Base64; +import java.util.Random; + +import com.bakdata.conquery.models.auth.web.AuthCookieFilter; +import com.google.common.base.Stopwatch; +import com.password4j.Hash; +import com.password4j.PBKDF2Function; +import com.password4j.Password; +import com.password4j.types.Hmac; +import jakarta.annotation.Priority; +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 lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.StringUtils; + +/** + * 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)
  • + *
+ * + * This filter is only installed on the ui side of the admin servlet and should to be evaluated before any authentication. + * Hence, its priority is lower. + */ +@Slf4j +@Priority(AuthCookieFilter.PRIORITY - 100) +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; + + /** + * This needs to be fast, because the hash is computed on every api request and it is only short-lived. + */ + private final static PBKDF2Function HASH_FUNCTION = PBKDF2Function.getInstance(Hmac.SHA256, 1000, 256); + public static final int COOKIE_MAX_AGE = 3600 /* seconds */; + public static final int SALT_LENGTH = 32; + + private final 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); + + if (StringUtils.isBlank(csrfToken)) { + log.warn("No csrf-token was registered for this request. Skipping csrf-cookie creation."); + return; + } + + final String csrfTokenHash = getTokenHash(csrfToken); + + log.trace("Hashed token for cookie. token='{}' hash='{}'", csrfToken, csrfTokenHash); + + responseContext.getHeaders() + .add(HttpHeaders.SET_COOKIE, new NewCookie( + CSRF_COOKIE_NAME, + csrfTokenHash, + "/", + null, + 0, + null, + COOKIE_MAX_AGE, + null, + requestContext.getSecurityContext().isSecure(), + false + )); + } + + private static String getTokenHash(String csrfToken) { + final Stopwatch stopwatch = log.isTraceEnabled() ? Stopwatch.createStarted() : null; + + final Hash hash = Password.hash(csrfToken).addRandomSalt(SALT_LENGTH).with(HASH_FUNCTION); + + log.trace("Generated token in {}", stopwatch); + + final String encodedSalt = Base64.getEncoder().encodeToString(hash.getSaltBytes()); + + // Use '_' as join char, because it is not part of the standard base64 encoding (in base64url though) + return String.join("_", encodedSalt, hash.getResult()); + } + + public static boolean checkHash(String token, String hash) { + int delimIdx; + if ((delimIdx = hash.indexOf("_")) == -1 || delimIdx == hash.length() - 1) { + throw new IllegalArgumentException("The provided hash must be of this form: _, was: " + hash); + } + final String encodedSalt = hash.substring(0, delimIdx); + final String saltedHash = hash.substring(delimIdx + 1); + + final byte[] salt = Base64.getDecoder().decode(encodedSalt); + final Stopwatch stopwatch = log.isTraceEnabled() ? Stopwatch.createStarted() : null; + final boolean decision = Password.check(token, saltedHash).addSalt(salt).with(HASH_FUNCTION); + + log.trace("Checked token in {}", stopwatch); + return decision; + } + + public static String getCsrfTokenProperty(ContainerRequestContext requestContext) { + return (String) requestContext.getProperty(CSRF_TOKEN_PROPERTY); + } +} diff --git a/backend/src/main/java/com/bakdata/conquery/models/auth/web/csrf/ReadMe.md b/backend/src/main/java/com/bakdata/conquery/models/auth/web/csrf/ReadMe.md new file mode 100644 index 0000000000..d1d36264e0 --- /dev/null +++ b/backend/src/main/java/com/bakdata/conquery/models/auth/web/csrf/ReadMe.md @@ -0,0 +1,24 @@ +# CSRF-Filter + +This filters prevents cross-site-request-forgery attempts which are conducted by luring a browser user to a malicious +website. +The user's browser needs be authenticated via an authentication cookie. +Because we have two servlets (UI + API) for the admin-end there are two different filters. + +The prevention method used is the +stateless [Signed Double-Submit Cookie](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#signed-double-submit-cookie-recommended). + +## [CsrfTokenSetFilter](./CsrfTokenSetFilter.java) + +This filter is installed on the UI-servlet. It generates a csrf-token and injects the plaintext token into the payload +and the signed/hashed token into a cookie. +The UI-servlet provides all html resources for the browser. + +## [CsrfTokenCheckFilter](./CsrfTokenCheckFilter.java) + +This filter is installed on the API-servlet. It extracts the double submitted token from a requests and validates them. +If the request did not contain a csrf token cookie (e.g. request was initiated by cURL) the filter does nothing. + +This filter is not installed on the UI-servlet. Even though this servlet's responses contain sensitive data, it only +provides GET-endpoints, which are not state altering and thus don't need csrf protection. On the API-servlet side we +validate all requests for simplicity. diff --git a/backend/src/main/java/com/bakdata/conquery/models/config/auth/IntrospectionDelegatingRealmFactory.java b/backend/src/main/java/com/bakdata/conquery/models/config/auth/IntrospectionDelegatingRealmFactory.java index 6220c3ba40..3501c01c67 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/config/auth/IntrospectionDelegatingRealmFactory.java +++ b/backend/src/main/java/com/bakdata/conquery/models/config/auth/IntrospectionDelegatingRealmFactory.java @@ -9,7 +9,7 @@ import com.bakdata.conquery.models.auth.basic.JWTokenHandler; import com.bakdata.conquery.models.auth.oidc.IntrospectionDelegatingRealm; import com.bakdata.conquery.models.auth.oidc.keycloak.KeycloakApi; -import com.bakdata.conquery.models.auth.web.DefaultAuthFilter; +import com.bakdata.conquery.models.auth.web.AuthFilter; import com.fasterxml.jackson.annotation.JsonIgnore; import com.nimbusds.oauth2.sdk.auth.ClientAuthentication; import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic; @@ -49,7 +49,7 @@ public class IntrospectionDelegatingRealmFactory extends Configuration { public ConqueryAuthenticationRealm createRealm(Environment environment, AuthorizationController authorizationController) { // Register token extractor for JWT Tokens - DefaultAuthFilter.registerTokenExtractor(JWTokenHandler.JWTokenExtractor.class, environment.jersey().getResourceConfig()); + AuthFilter.registerTokenExtractor(JWTokenHandler.JWTokenExtractor.class, environment.jersey().getResourceConfig()); // At start up, try tp retrieve the idp client api object if possible. If the idp service is not up don't fail start up. authClient = getAuthClient(false); diff --git a/backend/src/main/java/com/bakdata/conquery/models/config/auth/LocalAuthenticationConfig.java b/backend/src/main/java/com/bakdata/conquery/models/config/auth/LocalAuthenticationConfig.java index 4ea4956d42..cde3dea9fa 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/config/auth/LocalAuthenticationConfig.java +++ b/backend/src/main/java/com/bakdata/conquery/models/config/auth/LocalAuthenticationConfig.java @@ -13,7 +13,7 @@ import com.bakdata.conquery.models.auth.basic.JWTokenHandler; import com.bakdata.conquery.models.auth.basic.LocalAuthenticationRealm; import com.bakdata.conquery.models.auth.basic.UserAuthenticationManagementProcessor; -import com.bakdata.conquery.models.auth.web.DefaultAuthFilter; +import com.bakdata.conquery.models.auth.web.AuthFilter; import com.bakdata.conquery.models.auth.web.RedirectingAuthFilter; import com.bakdata.conquery.models.config.ConqueryConfig; import com.bakdata.conquery.models.config.XodusConfig; @@ -79,7 +79,7 @@ boolean isStorageEncrypted() { @Override public ConqueryAuthenticationRealm createRealm(Environment environment, ConqueryConfig config, AuthorizationController authorizationController) { // Token extractor is not needed because this realm depends on the ConqueryTokenRealm - DefaultAuthFilter.registerTokenExtractor(JWTokenHandler.JWTokenExtractor.class, environment.jersey().getResourceConfig()); + AuthFilter.registerTokenExtractor(JWTokenHandler.JWTokenExtractor.class, environment.jersey().getResourceConfig()); log.info("Performing benchmark for default hash function (bcrypt) with max_milliseconds={}", BCRYPT_MAX_MILLISECONDS); final BenchmarkResult result = SystemChecker.benchmarkBcrypt(BCRYPT_MAX_MILLISECONDS); 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..325d59e9c6 100644 --- a/backend/src/main/resources/assets/custom/js/script.js +++ b/backend/src/main/resources/assets/custom/js/script.js @@ -29,15 +29,32 @@ async function rest(url, options) { { method: 'get', credentials: 'same-origin', + ...options, + // Overwrite options[headers] here, BUT merge them with our headers headers: { - 'Content-Type': 'application/json' - }, - ...options + 'Content-Type': 'application/json', + // Csrf token is provided via global variable + 'X-Csrf-Token': csrf_token, + ...options?.headers, + } } ); 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; @@ -148,10 +165,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) { @@ -180,10 +196,9 @@ function postFile(event, url) { function postScriptHandler(event, jsonOut, responseTargetTextfield) { event.preventDefault(); responseTargetTextfield.innerHTML = 'waiting for response'; - fetch('/admin/script', + rest('/admin/script', { method: 'post', - credentials: 'same-origin', body: document.getElementById('script').value, headers: { 'Content-Type': 'text/plain', @@ -207,4 +222,85 @@ $("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, datasetId) { + 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/${datasetId}/${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); + }); +} + +function shutdown(event) { + event.preventDefault(); + rest( + '/tasks/shutdown', + { + method: 'post' + } + ); +} \ 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..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 @@ -27,8 +27,6 @@ <#macro idMapping>Here <@layout.layout> - - <@breadcrumbs.breadcrumbs @@ -51,7 +49,7 @@ - + @@ -32,43 +32,4 @@ - \ No newline at end of file diff --git a/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/group.html.ftl b/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/group.html.ftl index 402ffd0494..611ea50a5c 100644 --- a/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/group.html.ftl +++ b/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/group.html.ftl @@ -84,23 +84,20 @@ diff --git a/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/jobs.html.ftl b/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/jobs.html.ftl index 64db072311..17981be3f3 100644 --- a/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/jobs.html.ftl +++ b/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/jobs.html.ftl @@ -7,17 +7,16 @@ function cancelJob(jobId) { event.preventDefault(); - fetch( + rest( "/admin/jobs/" + jobId + "/cancel", { method: "post", - credentials: "same-origin" } ); } function getJobs() { - return fetch("/admin/jobs") + return rest("/admin/jobs") .then((res) => res.json()) .then((entries) => { const origins = {}; diff --git a/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/queries.html.ftl b/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/queries.html.ftl index 5bac320b71..c73c81cd19 100644 --- a/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/queries.html.ftl +++ b/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/queries.html.ftl @@ -1,159 +1,130 @@ <#import "templates/template.html.ftl" as layout> -<@layout.layout> + <@layout.layout> -

Queries

@@ -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/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/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 @@ + + + + ${title} <#nested /> diff --git a/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/templates/groupHandler.html.ftl b/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/templates/groupHandler.html.ftl index 17731552c4..4b89d2367a 100644 --- a/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/templates/groupHandler.html.ftl +++ b/backend/src/main/resources/com/bakdata/conquery/resources/admin/ui/templates/groupHandler.html.ftl @@ -40,22 +40,19 @@