Role-Based Access Control for Views
It’s possible to restrict access for selected Hilla views, based on roles defined for the logged-in user. This article explains how to do this.
To follow the examples here, you’ll need a Hilla application with authentication enabled. The Authentication With Spring Security page will help you to get started.
Define Roles with Spring Security
Roles are a set of string attributes representing the authorities that are assigned to a user. In Spring Security, the user details used for authentication also specify roles.
Typically, roles are defined in authority strings prefixed with ROLE_
. After successful authentication, these are accessible via the GrantedAuthority
objects returned by Authentication.getAuthorities()
. See the Authentication With Spring Security page for examples of configuration.
Using Roles in TypeScript
A convenient way to use roles for access control in TypeScript views is to add a Hilla endpoint that gets user information, including roles, from Java during authentication. To do this, first define a bean representing 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;
}
}
Next, add the endpoint to get a UserInfo
containing authorities for the logged-in user on the client side:
@BrowserCallable
public class UserInfoService {
@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 Authentication With Spring Security as follows:
import { UserInfoService } from 'Frontend/generated/endpoints';
import type UserInfo from 'Frontend/generated/com/vaadin/demo/fusion/security/authentication/UserInfo';
interface Authentication {
user: UserInfo;
}
let authentication: Authentication | undefined;
export async function login(
username: string,
password: string,
options: LoginOptions = {}
): Promise<LoginResult> {
return await loginImpl(username, password, {
...options,
async onSuccess() {
// Get user info from endpoint
const user = await UserInfoService.getUserInfo();
authentication = {
user,
};
},
});
}
Add an 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 allowed roles to be specified on the view routes, define an extended type ViewRoute
, that has a rolesAllowed
string, like so:
export type ViewRoute = Route & {
title?: string;
children?: ViewRoute[];
rolesAllowed?: string[];
};
Add a method to check 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 like this:
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 the isAuthorizedViewRoute()
helper defined earlier. Then use the filtered list of routes as menu items:
private get menuRoutes() {
return routes.filter((route) => route.title).filter(isAuthorizedViewRoute);
}