Skip to content

Commit

Permalink
fix: fix role checking when using websocket push (#20679) (#20709)
Browse files Browse the repository at this point in the history
When using PUSH with websocket transport, the atmosphere wrapped request
can be a no-op implementation whose isUserInRole method alwasy returns
false, causing, for example, wrong access checking during navigation.
This change falls back to Spring Securty for role checking when PUSH
transport is websocket.
It also fixes some tests in order to propagate the Spring Security context
when starting Thread that perform UI operations.

References psi#123
Part of #11026
  • Loading branch information
mcollovati authored Dec 16, 2024
1 parent 276ded1 commit 4b75f5b
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,7 @@ public NavigationContext createNavigationContext(Class<?> navigationTarget,
Objects.requireNonNull(vaadinService);
return new NavigationContext(vaadinService.getRouter(),
navigationTarget, new Location(path), RouteParameters.empty(),
vaadinRequest.getUserPrincipal(),
getRolesChecker(vaadinRequest), false);
getPrincipal(vaadinRequest), getRolesChecker(vaadinRequest),
false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,17 @@ public class AppViewIT extends com.vaadin.flow.spring.flowsecurity.AppViewIT {
session expiration handler to redirect to the timeout page instead
of the logout view, because the logout process is still ongoing.
""")
public void logout_via_doLogin_redirects_to_logout() {
super.logout_via_doLogin_redirects_to_logout();
public void logout_via_doLogoutURL_redirects_to_logout() {
super.logout_via_doLogoutURL_redirects_to_logout();
}

@Test
public void websocket_roles_checked_correctly_during_navigation() {
open("admin");
loginAdmin();
navigateTo("");
assertRootPageShown();
navigateTo("admin");
assertAdminPageShown(ADMIN_FULLNAME);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

import java.util.concurrent.TimeUnit;

import org.springframework.security.concurrent.DelegatingSecurityContextRunnable;
import org.springframework.security.core.context.SecurityContextHolder;

import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.html.Div;
Expand Down Expand Up @@ -37,14 +40,16 @@ public AdminView(SecurityUtils securityUtils) {
accessRolePrefixedAdminPageFromThread.setId(ROLE_PREFIX_TEST_BUTTON_ID);
accessRolePrefixedAdminPageFromThread.addClickListener(event -> {
UI ui = event.getSource().getUI().get();
new Thread(() -> {
Runnable doNavigation = () -> {
try {
TimeUnit.MILLISECONDS.sleep(100);
ui.access(() -> ui.navigate(RolePrefixedAdminView.class));
} catch (InterruptedException e) {
}

}).start();
};
Runnable wrappedRunnable = new DelegatingSecurityContextRunnable(
doNavigation, SecurityContextHolder.getContext());
new Thread(wrappedRunnable).start();
});
add(accessRolePrefixedAdminPageFromThread);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.vaadin.flow.spring.flowsecurity.views;

import org.springframework.security.concurrent.DelegatingSecurityContextRunnable;
import org.springframework.security.core.context.SecurityContextHolder;

import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.html.H1;
Expand Down Expand Up @@ -36,16 +39,19 @@ public PublicView() {
Button backgroundNavigation = new Button(
"Navigate to admin view in 1 second", e -> {
UI ui = e.getSource().getUI().get();
new Thread(() -> {
Runnable navigateToAdmin = () -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
}
ui.access(() -> {
ui.navigate(AdminView.class);
});

}).start();
};
Runnable wrappedRunnable = new DelegatingSecurityContextRunnable(
navigateToAdmin,
SecurityContextHolder.getContext());
new Thread(wrappedRunnable).start();
});
backgroundNavigation.setId(BACKGROUND_NAVIGATION_ID);
add(backgroundNavigation);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.apache.commons.io.IOUtils;
import org.junit.Assert;
import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;

import com.vaadin.flow.component.button.testbench.ButtonElement;
Expand All @@ -22,12 +23,12 @@

public class AppViewIT extends AbstractIT {

private static final String LOGIN_PATH = "my/login/page";
private static final String USER_FULLNAME = "John the User";
private static final String ADMIN_FULLNAME = "Emma the Admin";
protected static final String LOGIN_PATH = "my/login/page";
protected static final String USER_FULLNAME = "John the User";
protected static final String ADMIN_FULLNAME = "Emma the Admin";

private void logout() {
if (!$(ButtonElement.class).attribute("id", "logout").exists()) {
if (!$(ButtonElement.class).withAttribute("id", "logout").exists()) {
open("");
assertRootPageShown();
}
Expand Down Expand Up @@ -298,7 +299,7 @@ public void navigate_in_thread_with_access() {

// https://github.com/vaadin/flow/issues/7323
@Test
public void logout_via_doLogin_redirects_to_logout() {
public void logout_via_doLogoutURL_redirects_to_logout() {
open(LOGIN_PATH);
loginAdmin();
navigateTo("admin");
Expand Down Expand Up @@ -340,18 +341,19 @@ private void navigateToClientMenuList() {
assertPathShown("menu-list");
}

private void navigateTo(String path) {
protected void navigateTo(String path) {
navigateTo(path, true);
}

private void navigateTo(String path, boolean assertPathShown) {
protected void navigateTo(String path, boolean assertPathShown) {
getMainView().$("a").attribute("href", path).first().click();
if (assertPathShown) {
assertPathShown(path);
}
}

private TestBenchElement getMainView() {
waitForClientRouter();
return waitUntil(driver -> $("*").id("main-view"));
}

Expand Down Expand Up @@ -406,4 +408,16 @@ private void waitForUploads(UploadElement element, int maxSeconds) {
getCommandExecutor().getDriver().executeAsyncScript(script, element);
}

/*
* The same driver is used to access both Vaadin views and static resources.
* Static caching done by #isClientRouter can cause some tests to be flaky.
*/
protected void waitForClientRouter() {
boolean hasClientRouter = (boolean) executeScript(
"return !!window.Vaadin.Flow.clients.TypeScript");
if (hasClientRouter) {
waitForElementPresent(By.cssSelector("#outlet > *"));
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,16 @@

import java.security.Principal;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.function.Predicate;

import org.springframework.security.concurrent.DelegatingSecurityContextRunnable;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextImpl;

import com.vaadin.flow.di.Lookup;
import com.vaadin.flow.server.HandlerHelper;
import com.vaadin.flow.server.VaadinRequest;
import com.vaadin.flow.server.VaadinService;
import com.vaadin.flow.spring.AuthenticationUtil;
Expand All @@ -42,7 +49,9 @@ class SecurityUtil {
* if no user is currently logged in
*/
static Principal getPrincipal(VaadinRequest request) {
if (request == null) {
boolean isWebsocketPush = isWebsocketPush(request);
if (request == null
|| (isWebsocketPush && request.getUserPrincipal() == null)) {
return AuthenticationUtil.getSecurityHolderAuthentication();
}
return request.getUserPrincipal();
Expand All @@ -58,15 +67,40 @@ static Principal getPrincipal(VaadinRequest request) {
* the user is included in that role
*/
static Predicate<String> getRolesChecker(VaadinRequest request) {
if (request == null) {
return Optional.ofNullable(VaadinService.getCurrent())
boolean isWebsocketPush = isWebsocketPush(request);

// Role checks on PUSH request works out of the box only happen if
// transport is not WEBSOCKET.
// For websocket PUSH, HttServletRequest#isUserInRole method in
// Atmosphere HTTP request wrapper always returns, so we need to
// fall back to Spring Security.
if (request == null || isWebsocketPush) {
AtomicReference<Function<String, Boolean>> roleCheckerHolder = new AtomicReference<>();
Runnable roleCheckerLookup = () -> roleCheckerHolder.set(Optional
.ofNullable(request).map(VaadinRequest::getService)
.or(() -> Optional.ofNullable(VaadinService.getCurrent()))
.map(service -> service.getContext()
.getAttribute(Lookup.class))
.map(lookup -> lookup.lookup(VaadinRolePrefixHolder.class))
.map(VaadinRolePrefixHolder::getRolePrefix)
.map(AuthenticationUtil::getSecurityHolderRoleChecker)
.orElseGet(
AuthenticationUtil::getSecurityHolderRoleChecker)::apply;
AuthenticationUtil::getSecurityHolderRoleChecker));

Authentication authentication = AuthenticationUtil
.getSecurityHolderAuthentication();
// Spring Security context holder might not have been initialized
// for thread handling websocket message. If so, create a temporary
// security context based on the handshake request principal.
if (authentication == null && isWebsocketPush && request
.getUserPrincipal() instanceof Authentication requestAuthentication) {
roleCheckerLookup = new DelegatingSecurityContextRunnable(
roleCheckerLookup,
new SecurityContextImpl(requestAuthentication));
}

roleCheckerLookup.run();
return roleCheckerHolder.get()::apply;
}

// Update active role prefix if it's not set yet.
Expand All @@ -79,4 +113,12 @@ static Predicate<String> getRolesChecker(VaadinRequest request) {
return request::isUserInRole;
}

private static boolean isWebsocketPush(VaadinRequest request) {
return request != null
&& HandlerHelper.isRequestType(request,
HandlerHelper.RequestType.PUSH)
&& "websocket"
.equals(request.getHeader("X-Atmosphere-Transport"));
}

}

0 comments on commit 4b75f5b

Please sign in to comment.