Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: add documentation for navigation access control #2953

Merged
merged 24 commits into from
Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7f88c2f
docs: add documnetation for navigation access control
mcollovati Nov 9, 2023
1c228d9
additional docs
mcollovati Nov 10, 2023
05efa27
add diagram source file
mcollovati Nov 10, 2023
6bf4c6b
update decision resolver outputs table
mcollovati Nov 10, 2023
dda2f8b
fixes
mcollovati Nov 10, 2023
99dd528
Apply suggestions from code review
mcollovati Nov 10, 2023
f43bf68
mention denyall annotation
mcollovati Nov 10, 2023
22720de
First pass at editing.
russelljtdyer Nov 11, 2023
9cc7381
Second pass at editing: spring.adoc, navigation-access-control.adoc.
russelljtdyer Nov 11, 2023
ca8d06d
Third pass at editing: enabling-security.adoc
russelljtdyer Nov 11, 2023
5a470e5
Fourth pass at editing: securing-plain-java-app.adoc.
russelljtdyer Nov 11, 2023
db37b7e
Apply suggestions from code review
mcollovati Nov 13, 2023
5a10686
move nav access control to advanced topics
mcollovati Nov 13, 2023
085ec0b
fix page move
mcollovati Nov 13, 2023
e0b2356
new diagram
mcollovati Nov 13, 2023
c55249a
update diagram
mcollovati Nov 13, 2023
c48e40f
cleanup
mcollovati Nov 14, 2023
a6f5595
Apply suggestions from code review
mcollovati Nov 14, 2023
440a878
additional note about spring security integration
mcollovati Nov 23, 2023
5ad7718
align docs and examples with latest API
mcollovati Nov 23, 2023
cef5315
fixed example code
mcollovati Nov 29, 2023
e3ca47f
Apply suggestions from code review
mcollovati Dec 4, 2023
117020e
apply review suggetions
mcollovati Dec 4, 2023
bb718ff
Merge branch 'latest' into feat/navigation_access_control
mcollovati Dec 4, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/styles/Vocab/Docs/accept.txt
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ LitElement
Logback
Lumo
macOS
matcher
Maven
[mM]emoiz(e|ed|es)
[mM]iddleware
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
flowchart LR
Browser[Browser] --> NavigationAccessControl[Navigation Access Control]
NavigationAccessControl --> DelegateEvaluation((Evaluate\nnavigation\npermission))
DecisionResolver{Decision Resolver}
subgraph SecurityRules
direction TB
ViewAccessChecker[Annotated View Checker]
RoutePathAccessChecker[Route Path Checker]
CustomAccessChecker1[Custom Access Checker 1]
CustomAccessCheckerN[Custom Access Checker N]
end
DelegateEvaluation -.-> ViewAccessChecker
DelegateEvaluation -.-> RoutePathAccessChecker
DelegateEvaluation -.-> CustomAccessChecker1
DelegateEvaluation -.-> CustomAccessCheckerN
ViewAccessChecker -. Allow/Deny .-> DecisionResolver
RoutePathAccessChecker -. Allow/Deny .-> DecisionResolver
CustomAccessChecker1 -. Allow/Deny .-> DecisionResolver
CustomAccessCheckerN -. Allow/Deny .-> DecisionResolver
DecisionResolver --> Result[Allow or Deny]

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
268 changes: 268 additions & 0 deletions articles/security/advanced-topics/navigation-access-control.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
---
title: Navigation Access Control
description: Explains navigation access control and how it can be customized.
order: 60
---


= [since:com.vaadin:[email protected]]#Navigation Access Control#

Navigation Access Control is a Flow security feature in which you can allow or deny the navigation to a certain view based on different and pluggable rules. When active, the access control intercepts navigation events, evaluates all configured rules. It then decides whether the target view should be rendered, or if the access should be denied.

Navigation Access Control is an improvement and a replacement of the [deprecated:com.vaadin:[email protected]]#[classname]`ViewAccessChecker` mechanism#. It provides annotation-based view security. It also expands the capabilities of the former access checker by introducing a new path-based check and by allowing custom rules, which are set by implementing the [interfacename]`NavigationAccessChecker` interface.


== Architecture

The Navigation Access Control mechanism is composed of three fundamental components: [classname]`NavigationAccessControl`; [interfacename]`NavigationAccessChecker`; and [interfacename]`AccessCheckDecisionResolver`.

[classname]`NavigationAccessControl` is the entry point for navigation security. It listens for navigation events, and it evaluates all security rules. Based on the results, it allows or denies access to the target view.

[interfacename]`NavigationAccessChecker` interface implementers represent the security rules that are evaluated during a navigation event.

[interfacename]`AccessCheckDecisionResolver` has the responsibility of taking the final decision to allow or deny a navigation, based on the result of the evaluation of the security rules.

image::images/navigation_access_control.svg[Navigation Access Control Architecture]

[classname]`NavigationAccessControl` can be configured to evaluate multiple [interfacename]`NavigationAccessChecker`. For example, you can enable annotation-based view access check and path-based access check. During a navigation request, both checkers are evaluated against the current [classname]`NavigationContext` and they produce an [classname]`AccessCheckResult`.

The [interfacename]`AccessCheckDecisionResolver` analyzes the results and provides the final decision. The access control uses that decision to proceed with the navigation or reroute to the login view or to an error page.


=== Navigation Access Checkers

Vaadin Flow provides two implementations of [interfacename]`NavigationAccessChecker` interface: [classname]`AnnotatedViewAccessChecker` and [classname]`RoutePathAccessChecker`.

==== AnnotatedViewAccessChecker

[classname]`AnnotatedViewAccessChecker` works in the same way as the former [classname]`ViewAccessChecker`. It looks for security annotation ([annotationname]`@AnonymousAllowed`, [annotationname]`@PermitAll`, [annotationname]`@RolesAllowed`, or [annotationname]`@DenyAll`) on the target view class. It evaluates them against the current navigation context. [classname]`AnnotatedViewAccessChecker` delegates the check to an instance of [classname]`AccessAnnotationChecker`, that can be extended to provide custom rules.

==== RoutePathAccessChecker

[classname]`RoutePathAccessChecker` evaluates instead the path used to navigate to a view. It requires an implementation of [interfacename]`AccessPathChecker`, that is responsible for the effective path verification.

The Vaadin Spring add-on offers an [interfacename]`AccessPathChecker` implementation based on Spring Security configuration. The path used to navigate to the target view is evaluated using the Spring [interfacename]`WebInvocationPrivilegeEvaluator` component, that applies the rules defined by the request matchers configured for Spring Security.

This means that to protect a Flow route, you can configure a Spring request matchers matching the route path, and use the authorization helpers to define the access grants, as in the following example.

[source,java]
----
@Route("admin")
public class AdminView extends Div { }

@Route("protected/profile")
public class ProfileView extends Div { }

@Route("special/preview-feature")
public class ProfileView extends Div { }

@Route("")
public class HomeView extends Div { }

public class SecurityConfig extends VaadinWebSecurity {

@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
.requestMatchers(antMatchers("/admin")).hasAnyRole(ROLE_ADMIN)
.requestMatchers(antMatchers("/protected/**")).authenticated()
.requestMatchers(antMatchers("/special/**"))
.access((authentication, object) -> {
// Custom authorization logic
return new AuthorizationDecision(isPremiumUser(authentication));
})
.requestMatchers(antMatchers("/"))
.permitAll()
);
}
}
----


Integration with other security frameworks can be accomplished similarly by providing a specific [interfacename]`AccessPathChecker` implementation.

==== Implementing a Custom Navigation Access Checker

If the above checkers aren't enough to fulfill the project requirements, a custom [classname]`NavigationAccessChecker` can be implemented. [classname]`NavigationAccessChecker` has a single method that takes a [classname]`NavigationContext` as input and produces an [classname]`AccessCheckResult` as output.

The [classname]`NavigationContext` provides information about the current navigation, such as target view class and location, potential authenticated user and its roles.

The [classname]`AccessCheckResult` holds the decision taken by the checker -- and potentially an informative reason about the decision. The decision can be `ALLOW`, `DENY`, `NEUTRAL`, or `REJECT`. `ALLOW` and `DENY` are self-explanatory. `NEUTRAL` means that the checker doesn't have enough information to make a decision, and it delegates the responsibility to the other configured checkers. `REJECT` has the same meaning of `DENY`, but it's used to signal a critical situation where the checker cannot make the decision because of a system configuration error.

In development mode, a rejection is handled by throwing an exception so that the situation can be detected immediately. The [classname]`AccessCheckResult` object is created by calling its factory methods [methodname]`allow`, [methodname]`deny`, [methodname]`neutral`, or [methodname]`reject`. In alternative, similar methods can be called on [classname]`NavigationContext` instance.

For example, a navigation access check implementation that allows access to a voting view only when voting has been marked as open, could look like the following code:

[source,java]
----
include::{root}/src/main/java/com/vaadin/demo/flow/auth/VotingNavigationAccessChecker.java[tags=snippet]
----


==== Navigation Error Handling Phase

When implementing a custom [classname]`NavigationAccessChecker`, it's important to know that the checker is called also when the navigation is rerouted to an error handler view. The [classname]`NavigationContext` class provides the [methodname]`isErrorHandling()` method to verify if the call happens during a navigation error handling phase. In this case, a custom checker would probably abstain from making a decision for the internal navigation, by returning a `NEUTRAL` result.

.Neutral Result on Error Handling Phase
[source,java]
----
@Override
public AccessCheckResult check(NavigationContext context) {
if (context.isErrorHandling()) {
return AccessCheckResult.neutral();
}
....
}
----


=== Decision Resolver

The [interfacename]`AccessCheckDecisionResolver` component is responsible for analyzing the results provided by the navigation access checkers, and for computing the final decision to grant or deny access to a view.

The default implementation makes the decision by applying the following rules:

.Default Decision Resolver Rules
[cols="1,1"]
|===
| Navigation Access Checkers Results | Decision

| `ALL ALLOW` | `ALLOW`
| `ALLOW + NEUTRAL` | `ALLOW`
| `ALL DENY` | `DENY`
| `DENY + NEUTRAL` | `DENY`
| `ALL NEUTRAL` | `DENY`
| `ALLOW + DENY` | `REJECT`
| `ALLOW + DENY + NEUTRAL` | `REJECT`
|===

As shown in the above table, if the navigation access checkers do not agree on the decision -- excluding neutral votes -- the default resolver rejects the navigation, causing an exception to be thrown in development mode. This situation happens usually because of invalid security configurations. For example, a view may be annotated with [annotationname]`@AnnonymousAllowed`, but the Spring Security configuration has a request matcher for the view path that grants access only to an authenticated user.

In error handling phase, almost the same rule applies with the exception that if all the results are `NEUTRAL`, the access is granted.
The reason is that, in this case, the target of the navigation is supposed to be an error handler component that do not expose sensible information.

.Default Decision Resolver Rules For Error Handling Phase
[cols="1,1"]
|===
| Navigation Access Checkers Results | Decision

| `ALL ALLOW` | `ALLOW`
| `ALLOW + NEUTRAL` | `ALLOW`
| `ALL NEUTRAL` | `ALLOW`
| `ALL DENY` | `DENY`
| `DENY + NEUTRAL` | `DENY`
| `ALLOW + DENY` | `REJECT`
| `ALLOW + DENY + NEUTRAL` | `REJECT`
|===


If the default implementation doesn't fit the requirements of the project, you can implement your own [interfacename]`AccessCheckDecisionResolver`. The interface defines a single method that takes as input the results computed by the navigation access checker and the current navigation context. Its output is an [classname]`AccessCheckResult`.

In the following example, the decision resolver allows access to the view if at least one of the checkers provided an `ALLOW` result:

[source,java]
----
include::{root}/src/main/java/com/vaadin/demo/flow/auth/CustomDecisionResolver.java[tags=snippet]
----


== Configuration

Navigation Access Control is enabled automatically on Spring Boot projects using [classname]`VaadinWebSecurity`. To enable the feature in plain Java projects, follow the specific <<{articles}/security/advanced-topics/securing-plain-java-app#, documentation page>>.

This section shows how to customize Navigation Access Control for both Spring and plain Java projects.


=== Spring Projects

When using [classname]`VaadinWebSecurity` as base class for configuring Spring Security, the Navigation Access Control feature is enabled by default. You can disable it by overriding the [methodname]`enableNavigationAccessControl` method in [classname]`VaadinWebSecurity` to return `false`. For backward compatibility, only the [classname]`AnnotatedViewAccessChecker` is active by default.

For a fine-grained configuration, you can expose a bean of type [classname]`NavigationAccessControlConfigurer`. [classname]`NavigationAccessControlConfigurer` allows activating out-of-the-box navigation access checkers or adding new ones, providing a custom decision resolver, completely disable the functionality, etc.

[NOTE]
If [classname]`VaadinWebSecurity` is not used, exposing the [classname]`NavigationAccessControlConfigurer` bean is mandatory to activate navigation access control.

In the following example, Navigation Access Control is configured to activate route path checker, a custom checker instance and all available [interfacename]`NavigationAccessChecker` beans that extend [classname]`VotingNavigationAccessChecker`. It also provides a custom decision resolver.

.Customize NavigationAccessControl in Spring Project
[source,java]
----
@Bean
NavigationAccessControlConfigurer navigationAccessControlConfigurerCustomizer() {
return new NavigationAccessControlConfigurer()
.withRoutePathAccessChecker() // <1>
.withNavigationAccessChecker(new CustomChecker()) // <2>
.withAvailableNavigationAccessCheckers(checker ->
checker instanceof VotingNavigationAccessChecker // <3>
)
.withDecisionResolver(new CustomDecisionResolver()); // <4>
}
----
<1> Activates the route path access checker. The implementation is provided by the Vaadin Spring module and exposed as a bean.
<2> Adds a custom navigation access checker, by creating a new instance.
<3> Activates all registered navigation access checker bean, that matches the predicate.
<4> Sets a custom decision resolver.

[IMPORTANT]
If you add the bean definition method in a configuration class extending [classname]`VaadinWebSecurity`, the method must be declared `static`, to prevent bootstrap errors because of circular dependencies in bean definitions.

.Define NavigationAccessControlConfigurer Bean in VaadinWebSecurity Subclass
[source,java]
----
class SecurityConfig extends VaadinWebSecurity {
@Bean
static NavigationAccessControlConfigurer navigationAccessControlConfigurer() {
return new NavigationAccessControlConfigurer()
.withRoutePathAccessChecker()
.withDecisionResolver(new CustomDecisionResolver());
}
}
----

Consult the [classname]`NavigationAccessControlConfigurer` Javadoc for more information on how navigation access control can be customized.


=== Plain Java Projects

Navigation Access Control settings in plain Java projects is documented in this <<{articles}/security/advanced-topics/securing-plain-java-app#adding-vaadinserviceinitlistener, separate page>>.

To apply custom settings, you need to create the [classname]`NavigationAccessControl` instance, providing the list of navigation access checkers and the decision resolver to be used.

It's important to know that for plain Java projects, there is no out-of-the-box support for route path access checking. However, as described earlier, it is possible to implement a custom [interfacename]`AccessPathChecker` that integrates with the security framework used in the project.

Here is how you can implement the example of the previous paragraph, in a non-Spring Java project:

.Custom NavigationAccessControl in Plain Java Project
[source,java]
----
public class FooBarSecurityAccessPathChecker implements AccessPathChecker {
@Override
public boolean hasAccess(
String path, Principal principal, Predicate<String> roleChecker) {
// implementation omitted
}
}

public class NavigationAccessCheckerInitializer implements VaadinServiceInitListener {
mshabarov marked this conversation as resolved.
Show resolved Hide resolved

public NavigationControlAccessCheckerInitializer() {
accessControl = new NavigationAccessControl(List.of(
new RoutePathAccessChecker(new FooBarSecurityAccessPathChecker()),
new CustomChecker(),
new VotingNavigationAccessChecker()
), new CustomDecisionResolver());
accessControl.setLoginView(LoginView.class);
}
}
----


[discussion-id]`4164EB30-201D-4FD3-940F-03630A752AD5`


++++
<style>
[class^=PageHeader-module--descriptionContainer] {display: none;}
</style>
++++

Loading
Loading