diff --git a/vaadin-spring-tests/pom.xml b/vaadin-spring-tests/pom.xml index 3bd05397a..321fe86ae 100644 --- a/vaadin-spring-tests/pom.xml +++ b/vaadin-spring-tests/pom.xml @@ -56,6 +56,16 @@ vaadin-upload-flow ${vaadin.version} + + com.vaadin + vaadin-dialog-flow + ${vaadin.version} + + + com.vaadin + vaadin-notification-flow + ${vaadin.version} + com.vaadin vaadin-ordered-layout-flow diff --git a/vaadin-spring-tests/test-spring-security-flow/pom.xml b/vaadin-spring-tests/test-spring-security-flow/pom.xml index dda6c2210..720ab713e 100644 --- a/vaadin-spring-tests/test-spring-security-flow/pom.xml +++ b/vaadin-spring-tests/test-spring-security-flow/pom.xml @@ -36,10 +36,22 @@ com.vaadin vaadin-tabs-flow + + com.vaadin + vaadin-text-field-flow + com.vaadin vaadin-upload-flow + + com.vaadin + vaadin-dialog-flow + + + com.vaadin + vaadin-notification-flow + com.vaadin vaadin-login-flow diff --git a/vaadin-spring-tests/test-spring-security-flow/src/main/java/com/vaadin/flow/spring/flowsecurity/SecurityUtils.java b/vaadin-spring-tests/test-spring-security-flow/src/main/java/com/vaadin/flow/spring/flowsecurity/SecurityUtils.java index 3b4b7013f..30150f4de 100644 --- a/vaadin-spring-tests/test-spring-security-flow/src/main/java/com/vaadin/flow/spring/flowsecurity/SecurityUtils.java +++ b/vaadin-spring-tests/test-spring-security-flow/src/main/java/com/vaadin/flow/spring/flowsecurity/SecurityUtils.java @@ -22,6 +22,12 @@ public class SecurityUtils { public UserDetails getAuthenticatedUser() { SecurityContext context = SecurityContextHolder.getContext(); + if (context == null) { + throw new IllegalStateException("No security context available"); + } + if (context.getAuthentication() == null) { + return null; + } Object principal = context.getAuthentication().getPrincipal(); if (principal instanceof UserDetails) { UserDetails userDetails = (UserDetails) context.getAuthentication() diff --git a/vaadin-spring-tests/test-spring-security-flow/src/main/java/com/vaadin/flow/spring/flowsecurity/service/BankService.java b/vaadin-spring-tests/test-spring-security-flow/src/main/java/com/vaadin/flow/spring/flowsecurity/service/BankService.java index 634cc634e..9d1f7895a 100644 --- a/vaadin-spring-tests/test-spring-security-flow/src/main/java/com/vaadin/flow/spring/flowsecurity/service/BankService.java +++ b/vaadin-spring-tests/test-spring-security-flow/src/main/java/com/vaadin/flow/spring/flowsecurity/service/BankService.java @@ -19,13 +19,25 @@ public class BankService { private SecurityUtils utils; public void applyForLoan() { + applyForLoan(10000); + } + + public void applyForHugeLoan() { + applyForLoan(1000000); + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + } + } + + private void applyForLoan(int amount) { String name = utils.getAuthenticatedUser().getUsername(); Optional acc = accountRepository.findByOwner(name); if (!acc.isPresent()) { return; } Account account = acc.get(); - account.setBalance(account.getBalance().add(new BigDecimal("10000"))); + account.setBalance(account.getBalance().add(new BigDecimal(amount))); accountRepository.save(account); } @@ -34,4 +46,5 @@ public BigDecimal getBalance() { return accountRepository.findByOwner(name).map(Account::getBalance) .orElse(null); } + } diff --git a/vaadin-spring-tests/test-spring-security-flow/src/main/java/com/vaadin/flow/spring/flowsecurity/views/Broadcaster.java b/vaadin-spring-tests/test-spring-security-flow/src/main/java/com/vaadin/flow/spring/flowsecurity/views/Broadcaster.java new file mode 100644 index 000000000..d94e8fc6c --- /dev/null +++ b/vaadin-spring-tests/test-spring-security-flow/src/main/java/com/vaadin/flow/spring/flowsecurity/views/Broadcaster.java @@ -0,0 +1,46 @@ +/* + * Copyright 2000-2021 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.spring.flowsecurity.views; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventBus; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.shared.Registration; + +public class Broadcaster { + + private static Broadcaster instance = new Broadcaster(); + + private ComponentEventBus router = new ComponentEventBus(new Div()); + + public static class RefreshEvent extends ComponentEvent { + public RefreshEvent() { + super(new Div(), false); + } + } + + public static void sendMessage() { + instance.router.fireEvent(new RefreshEvent()); + } + + public static Registration addMessageListener( + ComponentEventListener listener) { + return instance.router.addListener(RefreshEvent.class, listener); + } + +} diff --git a/vaadin-spring-tests/test-spring-security-flow/src/main/java/com/vaadin/flow/spring/flowsecurity/views/PrivateView.java b/vaadin-spring-tests/test-spring-security-flow/src/main/java/com/vaadin/flow/spring/flowsecurity/views/PrivateView.java index a835d1927..3c5ed6f81 100644 --- a/vaadin-spring-tests/test-spring-security-flow/src/main/java/com/vaadin/flow/spring/flowsecurity/views/PrivateView.java +++ b/vaadin-spring-tests/test-spring-security-flow/src/main/java/com/vaadin/flow/spring/flowsecurity/views/PrivateView.java @@ -3,23 +3,34 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.concurrent.Executor; import javax.annotation.security.PermitAll; +import com.vaadin.flow.component.AttachEvent; import com.vaadin.flow.component.ClickEvent; +import com.vaadin.flow.component.DetachEvent; +import com.vaadin.flow.component.Text; +import com.vaadin.flow.component.UI; import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.dialog.Dialog; import com.vaadin.flow.component.html.H4; import com.vaadin.flow.component.html.Image; import com.vaadin.flow.component.html.Paragraph; import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.notification.Notification; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.upload.Upload; import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.Route; import com.vaadin.flow.server.StreamResource; +import com.vaadin.flow.shared.Registration; import com.vaadin.flow.spring.flowsecurity.SecurityUtils; import com.vaadin.flow.spring.flowsecurity.service.BankService; +import org.springframework.security.concurrent.DelegatingSecurityContextExecutor; + @Route(value = "private", layout = MainView.class) @PageTitle("Private View") @PermitAll @@ -28,15 +39,27 @@ public class PrivateView extends VerticalLayout { private BankService bankService; private Span balanceSpan = new Span(); private SecurityUtils utils; + private DelegatingSecurityContextExecutor executor; + private Registration registration; - public PrivateView(BankService bankService, SecurityUtils utils) { + public PrivateView(BankService bankService, SecurityUtils utils, + Executor executor) { this.bankService = bankService; this.utils = utils; + this.executor = new DelegatingSecurityContextExecutor(executor); updateBalanceText(); balanceSpan.setId("balanceText"); add(balanceSpan); add(new Button("Apply for a loan", this::applyForLoan)); + add(new Button("Apply for a huge loan", + this::applyForHugeLoanUsingExecutor)); + + Button globalRefresh = new Button("Send global refresh event", + e -> Broadcaster.sendMessage()); + globalRefresh.setId("sendRefresh"); + add(globalRefresh); + Upload upload = new Upload(); ByteArrayOutputStream imageStream = new ByteArrayOutputStream(); upload.setReceiver((filename, mimeType) -> { @@ -57,16 +80,57 @@ public PrivateView(BankService bankService, SecurityUtils utils) { add(upload); } + @Override + protected void onAttach(AttachEvent attachEvent) { + super.onAttach(attachEvent); + attachEvent.getUI().setPollInterval(1000); + registration = Broadcaster.addMessageListener(e -> { + getUI().get().access(() -> this.updateBalanceText()); + }); + } + + @Override + protected void onDetach(DetachEvent detachEvent) { + super.onDetach(detachEvent); + detachEvent.getUI().setPollInterval(-1); + registration.remove(); + } + private void updateBalanceText() { String name = utils.getAuthenticatedUserInfo().getFullName(); BigDecimal balance = bankService.getBalance(); this.balanceSpan.setText(String.format( "Hello %s, your bank account balance is $%s.", name, balance)); - } private void applyForLoan(ClickEvent e) { bankService.applyForLoan(); updateBalanceText(); } + + private void applyForHugeLoanUsingExecutor(ClickEvent e) { + Dialog waitDialog = new Dialog(); + waitDialog.add(new Text("Processing loan application...")); + waitDialog.open(); + UI ui = getUI().get(); + Runnable runnable = new Runnable() { + @Override + public void run() { + try { + bankService.applyForHugeLoan(); + } catch (Exception e) { + getUI().get().access(() -> { + Notification + .show("Application failed: " + e.getMessage()); + }); + + } + ui.access(() -> { + updateBalanceText(); + waitDialog.close(); + }); + } + }; + executor.execute(runnable); + } } diff --git a/vaadin-spring-tests/test-spring-security-flow/src/test/java/com/vaadin/flow/spring/flowsecurity/AbstractIT.java b/vaadin-spring-tests/test-spring-security-flow/src/test/java/com/vaadin/flow/spring/flowsecurity/AbstractIT.java new file mode 100644 index 000000000..46c5d4638 --- /dev/null +++ b/vaadin-spring-tests/test-spring-security-flow/src/test/java/com/vaadin/flow/spring/flowsecurity/AbstractIT.java @@ -0,0 +1,122 @@ +/* + * Copyright 2000-2021 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.spring.flowsecurity; + +import com.vaadin.flow.component.login.testbench.LoginFormElement; +import com.vaadin.flow.component.login.testbench.LoginOverlayElement; +import com.vaadin.flow.testutil.ChromeBrowserTest; +import com.vaadin.testbench.TestBenchElement; + +import org.junit.After; +import org.junit.Assert; +import org.openqa.selenium.WebDriver; + +public abstract class AbstractIT extends ChromeBrowserTest { + + private static final String ROOT_PAGE_HEADER_TEXT = "Welcome to the Java Bank of Vaadin"; + private static final String ANOTHER_PUBLIC_PAGE_HEADER_TEXT = "Another public view for testing"; + private static final int SERVER_PORT = 8888; + + @Override + protected int getDeploymentPort() { + return SERVER_PORT; + } + + @Override + protected String getRootURL() { + return super.getRootURL(); // + "/context"; + } + + @After + public void tearDown() { + if (getDriver() != null) { + checkForBrowserErrors(); + } + } + + private void checkForBrowserErrors() { + checkLogsForErrors(msg -> { + return msg.contains( + "admin-only/secret.txt - Failed to load resource: the server responded with a status of 403"); + }); + } + + protected void open(String path) { + open(path, getDriver()); + } + + protected void open(String path, WebDriver driver) { + driver.get(getRootURL() + "/" + path); + } + + protected void loginUser() { + login("john", "john"); + } + + protected void loginAdmin() { + login("emma", "emma"); + } + + protected void login(String username, String password) { + assertLoginViewShown(); + + LoginFormElement form = $(LoginOverlayElement.class).first() + .getLoginForm(); + form.getUsernameField().setValue(username); + form.getPasswordField().setValue(password); + form.submit(); + waitUntilNot(driver -> $(LoginOverlayElement.class).exists()); + } + + protected void assertLoginViewShown() { + assertPathShown("login"); + waitUntil(driver -> $(LoginOverlayElement.class).exists()); + } + + protected void assertRootPageShown() { + waitUntil(drive -> $("h1").attribute("id", "header").exists()); + String headerText = $("h1").id("header").getText(); + Assert.assertEquals(ROOT_PAGE_HEADER_TEXT, headerText); + } + + protected void assertAnotherPublicPageShown() { + waitUntil(drive -> $("h1").attribute("id", "header").exists()); + String headerText = $("h1").id("header").getText(); + Assert.assertEquals(ANOTHER_PUBLIC_PAGE_HEADER_TEXT, headerText); + } + + protected void assertPrivatePageShown(String fullName) { + assertPathShown("private"); + waitUntil(driver -> $("span").attribute("id", "balanceText").exists()); + String balance = $("span").id("balanceText").getText(); + Assert.assertTrue(balance.startsWith( + "Hello " + fullName + ", your bank account balance is $")); + } + + protected void assertAdminPageShown(String fullName) { + assertPathShown("admin"); + TestBenchElement welcome = waitUntil(driver -> $("*").id("welcome")); + String welcomeText = welcome.getText(); + Assert.assertEquals("Welcome to the admin page, " + fullName, + welcomeText); + } + + protected void assertPathShown(String path) { + waitUntil(driver -> driver.getCurrentUrl() + .equals(getRootURL() + "/" + path)); + } + +} diff --git a/vaadin-spring-tests/test-spring-security-flow/src/test/java/com/vaadin/flow/spring/flowsecurity/AppViewIT.java b/vaadin-spring-tests/test-spring-security-flow/src/test/java/com/vaadin/flow/spring/flowsecurity/AppViewIT.java index 39e2f0895..371c63319 100644 --- a/vaadin-spring-tests/test-spring-security-flow/src/test/java/com/vaadin/flow/spring/flowsecurity/AppViewIT.java +++ b/vaadin-spring-tests/test-spring-security-flow/src/test/java/com/vaadin/flow/spring/flowsecurity/AppViewIT.java @@ -9,49 +9,18 @@ import java.util.stream.Collectors; import com.vaadin.flow.component.button.testbench.ButtonElement; -import com.vaadin.flow.component.login.testbench.LoginFormElement; -import com.vaadin.flow.component.login.testbench.LoginOverlayElement; import com.vaadin.flow.component.upload.testbench.UploadElement; -import com.vaadin.flow.testutil.ChromeBrowserTest; import com.vaadin.testbench.TestBenchElement; import org.apache.commons.io.IOUtils; -import org.junit.After; import org.junit.Assert; import org.junit.Test; -public class AppViewIT extends ChromeBrowserTest { +public class AppViewIT extends AbstractIT { - private static final String ROOT_PAGE_HEADER_TEXT = "Welcome to the Java Bank of Vaadin"; - private static final String ANOTHER_PUBLIC_PAGE_HEADER_TEXT = "Another public view for testing"; - private static final int SERVER_PORT = 8888; private static final String USER_FULLNAME = "John the User"; private static final String ADMIN_FULLNAME = "Emma the Admin"; - @Override - protected int getDeploymentPort() { - return SERVER_PORT; - } - - @Override - protected String getRootURL() { - return super.getRootURL(); // + "/context"; - } - - @After - public void tearDown() { - if (getDriver() != null) { - checkForBrowserErrors(); - } - } - - private void checkForBrowserErrors() { - checkLogsForErrors(msg -> { - return msg.contains( - "admin-only/secret.txt - Failed to load resource: the server responded with a status of 403"); - }); - } - private void logout() { if (!$(ButtonElement.class).attribute("id", "logout").exists()) { open(""); @@ -65,10 +34,6 @@ private void clickLogout() { getMainView().$(ButtonElement.class).id("logout").click(); } - private void open(String path) { - getDriver().get(getRootURL() + "/" + path); - } - @Test public void menu_correct_for_anonymous() { open(""); @@ -297,63 +262,6 @@ private TestBenchElement getMainView() { return waitUntil(driver -> $("*").id("main-view")); } - private void assertLoginViewShown() { - assertPathShown("login"); - waitUntil(driver -> $(LoginOverlayElement.class).exists()); - } - - private void assertRootPageShown() { - waitUntil(drive -> $("h1").attribute("id", "header").exists()); - String headerText = $("h1").id("header").getText(); - Assert.assertEquals(ROOT_PAGE_HEADER_TEXT, headerText); - } - - private void assertAnotherPublicPageShown() { - waitUntil(drive -> $("h1").attribute("id", "header").exists()); - String headerText = $("h1").id("header").getText(); - Assert.assertEquals(ANOTHER_PUBLIC_PAGE_HEADER_TEXT, headerText); - } - - private void assertPrivatePageShown(String fullName) { - assertPathShown("private"); - waitUntil(driver -> $("span").attribute("id", "balanceText").exists()); - String balance = $("span").id("balanceText").getText(); - Assert.assertTrue(balance.startsWith( - "Hello " + fullName + ", your bank account balance is $")); - } - - private void assertAdminPageShown(String fullName) { - assertPathShown("admin"); - TestBenchElement welcome = waitUntil(driver -> $("*").id("welcome")); - String welcomeText = welcome.getText(); - Assert.assertEquals("Welcome to the admin page, " + fullName, - welcomeText); - } - - private void assertPathShown(String path) { - waitUntil(driver -> driver.getCurrentUrl() - .equals(getRootURL() + "/" + path)); - } - - private void loginUser() { - login("john", "john"); - } - - private void loginAdmin() { - login("emma", "emma"); - } - - private void login(String username, String password) { - assertLoginViewShown(); - - LoginFormElement form = $(LoginOverlayElement.class).first() - .getLoginForm(); - form.getUsernameField().setValue(username); - form.getPasswordField().setValue(password); - form.submit(); - waitUntilNot(driver -> $(LoginOverlayElement.class).exists()); - } - private void refresh() { getDriver().navigate().refresh(); } diff --git a/vaadin-spring-tests/test-spring-security-flow/src/test/java/com/vaadin/flow/spring/flowsecurity/UIAccessContextIT.java b/vaadin-spring-tests/test-spring-security-flow/src/test/java/com/vaadin/flow/spring/flowsecurity/UIAccessContextIT.java new file mode 100644 index 000000000..28e46a880 --- /dev/null +++ b/vaadin-spring-tests/test-spring-security-flow/src/test/java/com/vaadin/flow/spring/flowsecurity/UIAccessContextIT.java @@ -0,0 +1,82 @@ +/* + * Copyright 2000-2021 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.spring.flowsecurity; + +import com.vaadin.flow.component.button.testbench.ButtonElement; +import com.vaadin.flow.component.login.testbench.LoginFormElement; +import com.vaadin.flow.component.login.testbench.LoginOverlayElement; +import com.vaadin.testbench.HasElementQuery; +import com.vaadin.testbench.TestBenchElement; + +import org.junit.Assert; +import org.junit.Test; +import org.openqa.selenium.SearchContext; +import org.openqa.selenium.WebDriver; + +public class UIAccessContextIT extends AbstractIT { + + @Test + public void securityContextSetForUIAccess() throws Exception { + String expectedUserBalance = "Hello John the User, your bank account balance is $10000.00."; + String expectedAdminBalance = "Hello Emma the Admin, your bank account balance is $200000.00."; + + WebDriver adminBrowser = getDriver(); + try { + super.setup(); + open("private"); + loginUser(); + TestBenchElement balance = $("span").id("balanceText"); + Assert.assertEquals(expectedUserBalance, balance.getText()); + + open("private", adminBrowser); + HasElementQuery adminContext = new HasElementQuery() { + + @Override + public SearchContext getContext() { + return adminBrowser; + } + + }; + loginAdmin(adminContext); + TestBenchElement adminBalance = adminContext.$("span") + .id("balanceText"); + Assert.assertEquals(expectedAdminBalance, adminBalance.getText()); + + ButtonElement sendRefresh = $(ButtonElement.class) + .id("sendRefresh"); + sendRefresh.click(); + Assert.assertEquals(expectedUserBalance, balance.getText()); + Assert.assertEquals(expectedAdminBalance, adminBalance.getText()); + + ButtonElement adminSendRefresh = adminContext.$(ButtonElement.class) + .id("sendRefresh"); + adminSendRefresh.click(); + Assert.assertEquals(expectedUserBalance, balance.getText()); + Assert.assertEquals(expectedAdminBalance, adminBalance.getText()); + } finally { + adminBrowser.quit(); + } + } + + private void loginAdmin(HasElementQuery adminContext) { + LoginFormElement form = adminContext.$(LoginOverlayElement.class) + .first().getLoginForm(); + form.getUsernameField().setValue("emma"); + form.getPasswordField().setValue("emma"); + form.submit(); + } + +} diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinAwareSecurityContextHolderStrategy.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinAwareSecurityContextHolderStrategy.java new file mode 100644 index 000000000..7763a5fd9 --- /dev/null +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinAwareSecurityContextHolderStrategy.java @@ -0,0 +1,90 @@ +/* + * Copyright 2000-2021 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.flow.spring.security; + +import com.vaadin.flow.server.VaadinSession; +import org.springframework.lang.NonNull; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolderStrategy; +import org.springframework.security.core.context.SecurityContextImpl; +import org.springframework.security.web.context.HttpSessionSecurityContextRepository; + +import java.util.Optional; + +import static java.util.Objects.requireNonNull; + +/** + * A strategy that uses an available VaadinSession for retrieving the security + * context. + * + * Falls back to the default thread specific security context when no + * vaadinSession is available. + */ +public final class VaadinAwareSecurityContextHolderStrategy + implements SecurityContextHolderStrategy { + + private final ThreadLocal contextHolder = new ThreadLocal<>(); + + @Override + public void clearContext() { + contextHolder.remove(); + } + + @Override + @NonNull + public SecurityContext getContext() { + /* + * We prefer the vaadin session information over the threadlocal as it + * is more specific. It makes a huge difference if you for instance to + * `otherSessionUI.access` in a request thread. In this case the + * security context is expected to reflect the "otherSession" and not + * the current request. + */ + SecurityContext context = getFromVaadinSession() + .orElseGet(() -> contextHolder.get()); + if (context == null) { + context = createEmptyContext(); + contextHolder.set(context); + } + return context; + } + + @NonNull + private Optional getFromVaadinSession() { + VaadinSession session = VaadinSession.getCurrent(); + if (session == null) { + return Optional.empty(); + } + Object securityContext = session.getSession().getAttribute( + HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY); + if (securityContext instanceof SecurityContext) { + return Optional.of((SecurityContext) securityContext); + } else { + return Optional.empty(); + } + } + + @Override + public void setContext(@NonNull SecurityContext securityContext) { + contextHolder.set(requireNonNull(securityContext)); + } + + @Override + @NonNull + public SecurityContext createEmptyContext() { + return new SecurityContextImpl(); + } +} diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinWebSecurityConfigurerAdapter.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinWebSecurityConfigurerAdapter.java index 2050a77ab..2f01684b0 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinWebSecurityConfigurerAdapter.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinWebSecurityConfigurerAdapter.java @@ -32,6 +32,7 @@ import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.OrRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; @@ -85,6 +86,11 @@ public void configure(WebSecurity web) throws Exception { @Override protected void configure(HttpSecurity http) throws Exception { + // Use a security context holder that can find the context from Vaadin + // specific classes + SecurityContextHolder.setStrategyName( + VaadinAwareSecurityContextHolderStrategy.class.getName()); + // Vaadin has its own CSRF protection. // Spring CSRF is not compatible with Vaadin internal requests http.csrf().ignoringRequestMatchers( diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java index 612317d9b..bae363d28 100644 --- a/vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java +++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java @@ -88,6 +88,7 @@ protected Stream getExcludedPatterns() { "com\\.vaadin\\.flow\\.spring\\.scopes\\.VaadinSessionScope", "com\\.vaadin\\.flow\\.spring\\.scopes\\.AbstractScope", "com\\.vaadin\\.flow\\.spring\\.scopes\\.VaadinUIScope", + "com\\.vaadin\\.flow\\.spring\\.security\\.VaadinAwareSecurityContextHolderStrategy", "com\\.vaadin\\.flow\\.spring\\.security\\.VaadinWebSecurityConfigurerAdapter", "com\\.vaadin\\.flow\\.spring\\.security\\.VaadinDefaultRequestCache", "com\\.vaadin\\.flow\\.spring\\.security\\.VaadinSavedRequestAwareAuthenticationSuccessHandler",
+ * Falls back to the default thread specific security context when no + * vaadinSession is available. + */ +public final class VaadinAwareSecurityContextHolderStrategy + implements SecurityContextHolderStrategy { + + private final ThreadLocal contextHolder = new ThreadLocal<>(); + + @Override + public void clearContext() { + contextHolder.remove(); + } + + @Override + @NonNull + public SecurityContext getContext() { + /* + * We prefer the vaadin session information over the threadlocal as it + * is more specific. It makes a huge difference if you for instance to + * `otherSessionUI.access` in a request thread. In this case the + * security context is expected to reflect the "otherSession" and not + * the current request. + */ + SecurityContext context = getFromVaadinSession() + .orElseGet(() -> contextHolder.get()); + if (context == null) { + context = createEmptyContext(); + contextHolder.set(context); + } + return context; + } + + @NonNull + private Optional getFromVaadinSession() { + VaadinSession session = VaadinSession.getCurrent(); + if (session == null) { + return Optional.empty(); + } + Object securityContext = session.getSession().getAttribute( + HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY); + if (securityContext instanceof SecurityContext) { + return Optional.of((SecurityContext) securityContext); + } else { + return Optional.empty(); + } + } + + @Override + public void setContext(@NonNull SecurityContext securityContext) { + contextHolder.set(requireNonNull(securityContext)); + } + + @Override + @NonNull + public SecurityContext createEmptyContext() { + return new SecurityContextImpl(); + } +} diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinWebSecurityConfigurerAdapter.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinWebSecurityConfigurerAdapter.java index 2050a77ab..2f01684b0 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinWebSecurityConfigurerAdapter.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinWebSecurityConfigurerAdapter.java @@ -32,6 +32,7 @@ import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.OrRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; @@ -85,6 +86,11 @@ public void configure(WebSecurity web) throws Exception { @Override protected void configure(HttpSecurity http) throws Exception { + // Use a security context holder that can find the context from Vaadin + // specific classes + SecurityContextHolder.setStrategyName( + VaadinAwareSecurityContextHolderStrategy.class.getName()); + // Vaadin has its own CSRF protection. // Spring CSRF is not compatible with Vaadin internal requests http.csrf().ignoringRequestMatchers( diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java index 612317d9b..bae363d28 100644 --- a/vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java +++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java @@ -88,6 +88,7 @@ protected Stream getExcludedPatterns() { "com\\.vaadin\\.flow\\.spring\\.scopes\\.VaadinSessionScope", "com\\.vaadin\\.flow\\.spring\\.scopes\\.AbstractScope", "com\\.vaadin\\.flow\\.spring\\.scopes\\.VaadinUIScope", + "com\\.vaadin\\.flow\\.spring\\.security\\.VaadinAwareSecurityContextHolderStrategy", "com\\.vaadin\\.flow\\.spring\\.security\\.VaadinWebSecurityConfigurerAdapter", "com\\.vaadin\\.flow\\.spring\\.security\\.VaadinDefaultRequestCache", "com\\.vaadin\\.flow\\.spring\\.security\\.VaadinSavedRequestAwareAuthenticationSuccessHandler",