Skip to content

Commit

Permalink
refactor: use MenuAccessControl to check view access (#20269) (#20307)
Browse files Browse the repository at this point in the history
Co-authored-by: Marco Collovati <[email protected]>
  • Loading branch information
vaadin-bot and mcollovati authored Oct 23, 2024
1 parent 585420e commit e15bb33
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -324,14 +324,16 @@ public static List<String> getClientRoutes(boolean filterClientViews,
public static Map<String, AvailableViewInfo> collectClientMenuItems(
boolean filterClientViews, AbstractConfiguration configuration,
VaadinRequest vaadinRequest) {

VaadinService vaadinService = Optional.ofNullable(vaadinRequest)
.map(VaadinRequest::getService)
.orElseGet(VaadinService::getCurrent);
Map<String, AvailableViewInfo> configurations = new HashMap<>();

collectClientMenuItems(configuration).forEach(
viewInfo -> collectClientViews("", viewInfo, configurations));

if (filterClientViews && !configurations.isEmpty()) {
filterClientViews(configurations, vaadinRequest);
filterClientViews(configurations, vaadinService);
}

return configurations;
Expand Down Expand Up @@ -480,9 +482,7 @@ public static URL getViewsJsonAsResource(

private static void filterClientViews(
Map<String, AvailableViewInfo> configurations,
VaadinRequest vaadinRequest) {
final boolean isUserAuthenticated = vaadinRequest
.getUserPrincipal() != null;
VaadinService vaadinService) {

Set<String> clientEntries = new HashSet<>(configurations.keySet());
for (String key : clientEntries) {
Expand All @@ -491,8 +491,8 @@ private static void filterClientViews(
continue;
}
AvailableViewInfo viewInfo = configurations.get(key);
boolean routeValid = validateViewAccessible(viewInfo,
isUserAuthenticated, vaadinRequest::isUserInRole);
boolean routeValid = vaadinService.getInstantiator()
.getMenuAccessControl().canAccessView(viewInfo);

if (!routeValid) {
configurations.remove(key);
Expand Down Expand Up @@ -528,31 +528,6 @@ private static boolean hasRequiredParameter(AvailableViewInfo viewInfo) {
return false;
}

/**
* Check view against authentication state.
* <p>
* If not authenticated and login required -> invalid. If user doesn't have
* correct roles -> invalid.
*
* @param viewInfo
* view info
* @param isUserAuthenticated
* user authentication state
* @param roleAuthentication
* method to authenticate if user has role
* @return true if accessible, false if something is not authenticated
*/
private static boolean validateViewAccessible(AvailableViewInfo viewInfo,
boolean isUserAuthenticated,
Predicate<? super String> roleAuthentication) {
if (viewInfo.loginRequired() && !isUserAuthenticated) {
return false;
}
String[] roles = viewInfo.rolesAllowed();
return roles == null || roles.length == 0
|| Arrays.stream(roles).anyMatch(roleAuthentication);
}

/**
* Get the current thread ContextClassLoader.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@

package com.vaadin.flow.server.auth;

import java.util.Optional;

/**
* Default implementation of {@link MenuAccessControl}.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@
package com.vaadin.flow.server.auth;

import java.io.Serializable;
import java.security.Principal;
import java.util.Arrays;
import java.util.function.Predicate;

import com.vaadin.flow.server.VaadinRequest;
import com.vaadin.flow.server.menu.AvailableViewInfo;

/**
* Interface for controlling access to routes in the application's menu
Expand Down Expand Up @@ -65,4 +71,51 @@ enum PopulateClientMenu {
*/
PopulateClientMenu getPopulateClientSideMenu();

/**
* Determines if current user has permissions to access the given view.
* <p>
* </p>
* It checks view against authentication state: - If view does not require
* login -> allow - If not authenticated and login required -> deny. - If
* user doesn't have correct roles -> deny.
*
* @param viewInfo
* view info
* @return true if the view is accessible, false if something is not
* authenticated.
*/
default boolean canAccessView(AvailableViewInfo viewInfo) {
VaadinRequest request = VaadinRequest.getCurrent();
if (request == null) {
return !viewInfo.loginRequired();
}
return canAccessView(viewInfo, request.getUserPrincipal(),
request::isUserInRole);
}

/**
* Check view against authentication state.
* <p>
* If not authenticated and login required -> invalid. If user doesn't have
* correct roles -> invalid.
*
* @param viewInfo
* view info
* @param principal
* current user, can be {@literal null}
* @param roleChecker
* function to authenticate if user has role
* @return true if accessible, false if something is not authenticated
*/
static boolean canAccessView(AvailableViewInfo viewInfo,
Principal principal, Predicate<String> roleChecker) {
boolean isUserAuthenticated = principal != null;
if (viewInfo.loginRequired() && !isUserAuthenticated) {
return false;
}
String[] roles = viewInfo.rolesAllowed();
return roles == null || roles.length == 0
|| Arrays.stream(roles).anyMatch(roleChecker);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
package com.vaadin.flow.spring;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
Expand All @@ -29,6 +31,7 @@
import com.vaadin.flow.server.startup.ApplicationConfigurationFactory;
import com.vaadin.flow.spring.SpringLookupInitializer.SpringApplicationContextInit;
import com.vaadin.flow.spring.i18n.DefaultI18NProviderFactory;
import com.vaadin.flow.spring.security.SpringMenuAccessControl;

/**
* Vaadin Application Spring configuration.
Expand Down Expand Up @@ -90,7 +93,22 @@ public DefaultI18NProvider vaadinI18nProvider(
*/
@Bean
@ConditionalOnMissingBean(value = MenuAccessControl.class)
@ConditionalOnMissingClass("org.springframework.security.core.context.SecurityContextHolder")
public MenuAccessControl vaadinMenuAccessControl() {
return new DefaultMenuAccessControl();
}

/**
* Creates default {@link MenuAccessControl}. This is created only if
* there's no {@link MenuAccessControl} bean declared.
*
* @return default menu access control
*/
@Bean
@ConditionalOnMissingBean(value = MenuAccessControl.class)
@ConditionalOnClass(name = "org.springframework.security.core.context.SecurityContextHolder")
public MenuAccessControl springSecurityVaadinMenuAccessControl() {
return new SpringMenuAccessControl();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright 2000-2024 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 java.security.Principal;
import java.util.Optional;
import java.util.function.Predicate;

import com.vaadin.flow.di.Lookup;
import com.vaadin.flow.server.VaadinRequest;
import com.vaadin.flow.server.VaadinService;
import com.vaadin.flow.spring.AuthenticationUtil;

/**
* Helper class to access user information and perform roles checks.
* <p>
* For internal use only. May be renamed or removed in a future release.
*/
class SecurityUtil {

/**
* Gets the principal for the currently logged in user.
*
* @param request
* the current request or {@code null} if no request is in
* progress (e.g. in a background thread)
* @return a representation of the currently logged in user or {@code null}
* if no user is currently logged in
*/
static Principal getPrincipal(VaadinRequest request) {
if (request == null) {
return AuthenticationUtil.getSecurityHolderAuthentication();
}
return request.getUserPrincipal();
}

/**
* Gets a function for checking roles for the currently logged in user.
*
* @param request
* the current request or {@code null} if no request is in
* progress (e.g. in a background thread)
* @return a function which takes a role name and returns {@code true} if
* the user is included in that role
*/
static Predicate<String> getRolesChecker(VaadinRequest request) {
if (request == null) {
return 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;
}

// Update active role prefix if it's not set yet.
Optional.ofNullable(request.getService())
.map(service -> service.getContext().getAttribute(Lookup.class))
.map(lookup -> lookup.lookup(VaadinRolePrefixHolder.class))
.filter(prefixHolder -> !prefixHolder.isSet()).ifPresent(
prefixHolder -> prefixHolder.resetRolePrefix(request));

return request::isUserInRole;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright 2000-2024 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.VaadinRequest;
import com.vaadin.flow.server.auth.DefaultMenuAccessControl;
import com.vaadin.flow.server.auth.MenuAccessControl;
import com.vaadin.flow.server.menu.AvailableViewInfo;

/**
* A Spring specific menu access control that falls back to Spring mechanisms
* for view access checking, when the generic mechanisms do not work.
* <p>
* </p>
* In Spring Boot application, a {@link SpringMenuAccessControl} is provided by
* default, if Spring Security is available.
*/
public class SpringMenuAccessControl extends DefaultMenuAccessControl {

@Override
public boolean canAccessView(AvailableViewInfo viewInfo) {
VaadinRequest request = VaadinRequest.getCurrent();
return MenuAccessControl.canAccessView(viewInfo,
SecurityUtil.getPrincipal(request),
SecurityUtil.getRolesChecker(request));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,14 @@

import java.security.Principal;
import java.util.Collection;
import java.util.Optional;
import java.util.function.Predicate;

import com.vaadin.flow.di.Lookup;
import com.vaadin.flow.server.VaadinRequest;
import com.vaadin.flow.server.VaadinService;
import com.vaadin.flow.server.auth.AccessCheckDecisionResolver;
import com.vaadin.flow.server.auth.AnnotatedViewAccessChecker;
import com.vaadin.flow.server.auth.DefaultAccessCheckDecisionResolver;
import com.vaadin.flow.server.auth.NavigationAccessChecker;
import com.vaadin.flow.server.auth.NavigationAccessControl;
import com.vaadin.flow.spring.AuthenticationUtil;

/**
* A Spring specific navigation access control that falls back to Spring
Expand Down Expand Up @@ -55,33 +51,12 @@ public SpringNavigationAccessControl(

@Override
protected Principal getPrincipal(VaadinRequest request) {
if (request == null) {
return AuthenticationUtil.getSecurityHolderAuthentication();
}
return super.getPrincipal(request);
return SecurityUtil.getPrincipal(request);
}

@Override
protected Predicate<String> getRolesChecker(VaadinRequest request) {
if (request == null) {
return 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;
}

// Update active role prefix if it's not set yet.
Optional.ofNullable(VaadinService.getCurrent())
.map(service -> service.getContext().getAttribute(Lookup.class))
.map(lookup -> lookup.lookup(VaadinRolePrefixHolder.class))
.filter(prefixHolder -> !prefixHolder.isSet()).ifPresent(
prefixHolder -> prefixHolder.resetRolePrefix(request));

return super.getRolesChecker(request);
return SecurityUtil.getRolesChecker(request);
}

}

0 comments on commit e15bb33

Please sign in to comment.