Role-Based Access Control for Views

This page describes how to restrict access for selected Fusion views based on roles defined for the logged-in user.

Starting Point

To follow examples in this article you need a Fusion application with authentication enabled. Follow the Authentication With Spring Security article to get prepared.

Define Roles for Users with Spring Security

Roles are, in the nutshell, a set of string attributes representing authorities that are assigned to a user.

In Spring Security, the user details used for authentication also provide roles. Typically, roles are stored as ROLE_ prefixed string authorities. After successful authentication, the GrantedAuthority objects returned by Authentication.getAuthorities() represent those. See the Authentication With Spring Security article for the configuration examples.

Using Roles in TypeScript

For using roles for access control of TypeScript views, it is convenient to add a Fusion endpoint that obtains user information with roles from Java during authentication.

First, define a bean representing the information about the user:

public class UserInfo {

    @Nonnull
    private String name;
    @Nonnull
    private Collection<String> authorities;

    public UserInfo(String name, Collection<String> authorities) {
        this.name = name;
        this.authorities = Collections.unmodifiableCollection(authorities);
    }

    public String getName() {
        return name;
    }

    public Collection<String> getAuthorities() {
        return authorities;
    }

}

After that, add the endpoint to get UserInfo with authorities for the logged-in user in the client side:

@Endpoint
public class UserInfoEndpoint {

    @PermitAll
    @Nonnull
    public UserInfo getUserInfo() {
        Authentication auth = SecurityContextHolder.getContext()
                .getAuthentication();

        final List<String> authorities = auth.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.toList());

        return new UserInfo(auth.getName(), authorities);
    }

}

Then, change the authentication implementation in TypeScript to get the user information from the endpoint. Change the auth.ts defined in the Authentication With Spring Security article as follows:

import { UserInfoEndpoint } from 'Frontend/generated/UserInfoEndpoint';
import UserInfo from 'Frontend/generated/com/vaadin/demo/fusion/security/authentication/UserInfo';

interface Authentication {
  user: UserInfo;
}

let authentication: Authentication | undefined = undefined;

export async function login(username: string, password: string): Promise<LoginResult> {
  const result = await loginImpl(username, password);
  if (!result.error) {
    // Get user info from endpoint
    const user = await UserInfoEndpoint.getUserInfo();
    authentication = {
      user,
    };
  }

  return result;
}

Add isUserInRole helper, which enables role-based access control checks for the UI.

export function isUserInRole(role: string) {
  if (!authentication) {
    return false;
  }

  return authentication.user.authorities.includes(`ROLE_${role}`);
}

Routes With Access Control

To enable specifying allowed roles on the view routes, define an extended type ViewRoute that has a rolesAllowed string as follows:

export type ViewRoute = Route & {
  title?: string;
  children?: ViewRoute[];
  rolesAllowed?: string[];
};

Add a method for checking access for the given route by iterating rolesAllowed using isUserInRole, as follows:

export function isAuthorizedViewRoute(route: ViewRoute) {
  if (route.rolesAllowed) {
    return route.rolesAllowed.find((role) => isUserInRole(role));
  }

  return true;
}

Then use the method added in the route action to redirect on unauthorized access, as follows:

export const routes: ViewRoute[] = [
  {
    path: 'protected',
    component: 'protected-view',
    title: 'Protected',
    rolesAllowed: ['ADMIN'],
    action: async (context, commands: Router.Commands) => {
      const route = context.route as ViewRoute;
      if (!isAuthorizedViewRoute(route)) {
        return commands.prevent();
      }
      await import('./protected-view');
      return undefined;
    },
  },
];

Hiding Unauthorized Menu Items

Filter the route list using isAuthorizedViewRoute helper defined above. Then use the filtered list of routes as menu items:

private get menuRoutes() {
    return routes.filter((route) => route.title).filter(isAuthorizedViewRoute);
  }