# Role Based Access Controls
# Overview
Experts recommend following a practice known as Defense in Depth where security controls are placed in each layer of an architecture. This tutorial guides you through adding access controls to your existing Entando project, in both the frontend and backend of your Entando Application.
The simple Conference application found in the Generate Microservices and Micro Frontends tutorial is used as a starting point. We recommend working through that tutorial for background and context.
The basic security setup for a blueprint-generated application allows any authenticated user to access the functionality contained in the MFEs and/or microservices. This tutorial defines two user roles for our application:
conference-user
: Permitted to view the Conferences in the tableWidgetconference-admin
: Permitted to view Conferences in the tableWidget, and also to delete Conferences from the tableWidget
# Apply and Verify Access Controls
# Step 1: Secure the Conference list
The list of Conferences must be visible to only the conference-user
and conference-admin
user roles.
- Go to
microservices/conference-ms/src/main/java/com/YOUR-ORG/YOUR-APP-NAME/web/rest
- Open
ConferenceResource.java
- Add the following to the list of imports:
import org.springframework.security.access.prepost.PreAuthorize;
- Modify the REST API
Conference:getAllConferences
method by preceding it with the @PreAuthorize annotation shown here:
@PreAuthorize("hasAnyAuthority('conference-user','conference-admin')")
This confines the use of the getAllConferences
method to users who are assigned either the conference-user
or the conference-admin
role on the Keycloak client configured for the microservice. Your method signature may be different depending on your blueprint selections, but this is an example of the updated section:
@GetMapping("/conferences")
@PreAuthorize("hasAnyAuthority('conference-user','conference-admin')")
public ResponseEntity<List<Conference>> getAllConferences(@org.springdoc.api.annotations.ParameterObject Pageable pageable) {
# Step 2: Run your project locally
The following commands must be run from your bundle project directory. They leverage the ent CLI.
Note: Refer to the Run Blueprint-generated Microservices and Micro Frontends in Dev Mode tutorial for more details.
- Start up your Keycloak instance
ent bundle svc start keycloak
- Start the microservice
ent bundle run conference-ms
- Start the conference-table MFE in another shell
ent bundle run conference-table
# Step 3: Access the conference-table MFE
- In your browser, go to http://localhost:3000 (opens new window)
- Access the conference-table MFE with the default credentials of
username: admin
,password: admin
Note: Once authenticated, the message "No conferences are available" is generated. If you check your browser console, you should see a 403 (Forbidden)
error for the request made to localhost:8080/services/conference/api/conferences
. This is expected because the admin
user does not yet have the necessary role.
# Step 4: Login to Keycloak
- Go to http://localhost:9080 (opens new window)
- Log in using the default credentials of
username: admin
,password: admin
# Step 5: Create the conference-user
and conference-admin
roles
- Go to
Clients
→internal
→Roles
- Click
Add Role
- Fill in the
Role Name
withconference-user
- Click
Save
- Repeat these steps to create the
conference-admin
role
Note: The
internal
client is configured by default in the Spring Bootapplication.yml
.
# Step 6: Map the conference-user
role to the admin
user
To grant access to the getAllConferences
API:
- Go to
Users
→View all users
→admin
→Role Mappings
- Select
internal
for theClient Roles
- Move
conference-user
fromAvailable Roles
toAssigned Roles
- Return to the MFE to confirm you can now see the full list of Conferences
# Step 7: Restrict the ability to delete Conferences
The conference-admin
role should grant a user permission to delete Conferences. To restrict the delete method to the conference-admin
role:
- Go to the
src/main/java/com/YOUR-ORG/YOUR-APP-NAME/web/rest
directory - Open
ConferenceResource.java
- Modify the
deleteConference
method by preceding it with the following annotation:
@PreAuthorize("hasAuthority('conference-admin')")
The resulting code section should look similar to this:
@DeleteMapping("/conferences/{id}")
@PreAuthorize("hasAuthority('conference-admin')")
public ResponseEntity<Void> deleteConference(@PathVariable Long id) {
log.debug("REST request to delete Conference : {}", id);
conferenceRepository.deleteById(id);
return ResponseEntity
.noContent()
.headers(HeaderUtil.createEntityDeletionAlert(applicationName, true, ENTITY_NAME, id.toString()))
.build();
To verify that a user without the conference-admin
role is unable to call the delete API:
- Restart the microservice. By default, this includes rebuilding any changed source files.
- Return to the MFE and try deleting one of the Conferences in the list
- Verify that attempting to delete a Conference via the UI generates a
403 error
in the browser console. There should be an error in the service logs similar to the following:
WARN 3208 --- [ XNIO-1 task-3] o.z.problem.spring.common.AdviceTraits : Forbidden: Access is denied
# Step 8: Hide the delete button
The MFE UI can be updated to hide the delete button from a user without the conference-admin
authority. The key logic checks whether the internal
client role conference-admin
is mapped to the current user via the hasResourceRole call.
- Go to the
microfrontends/conference-table/src/components
directory - Open
ConferenceTableContainer.js
- Replace the
onDelete
logic with an additional user permission:
const isAdmin = (keycloak && keycloak.authenticated) ? keycloak.hasResourceRole("conference-admin", "internal"): false;
const showDelete = onDelete && isAdmin;
const Actions = ({ item }) =>
showDelete ? (
The resulting code section should look similar to this:
render() {
const { items, count, notificationMessage, notificationStatus, filters } = this.state;
const { classes, onSelect, onAdd, onDelete, t, keycloak, paginationMode = '' } = this.props;
const deleteLabel = t('common.delete');
const isAdmin = (keycloak && keycloak.authenticated) ? keycloak.hasResourceRole("conference-admin", "internal"): false;
const showDelete = onDelete && isAdmin;
const Actions = ({ item }) =>
showDelete ? (
<ConfirmationDialogTrigger
onCloseDialog={(action) => this.handleConfirmationDialogAction(action, item)}
dialog={{
title: t('entities.conference.deleteDialog.title'),
description: t('entities.conference.deleteDialog.description'),
...
- Confirm that the delete icon is no longer visible in the MFE. The MFE should have automatically reloaded to reflect the code changes.
# Step 9: Grant and verify delete permissions
Promote the admin user to a full conference-admin
to reinstate the ability to delete Conferences.
- Return to Keycloak at http://localhost:9080 (opens new window)
- Go to
Users
→View all users
→admin
→Role Mappings
- Select
internal
underClient Roles
, and add theconference-admin
role to theAssigned
column. - Reload the MFE
- Confirm the delete icon is visible
- Confirm a Conference can be successfully deleted from the list
# Step 10. Configure the roles in entando.json
Entando can automatically add roles to your client (see the notes below for different client options) when your microservice is deployed.
- Modify
entando.json
by adding the following line to themicroservices/conference-ms
:
"roles": ["conference-admin","conference-user"]
# Next Steps
Follow one of the links below to run the bundle components locally, or build and publish the bundle into an Entando Application:
- Run Blueprint-generated components locally in dev mode
- Build and publish a project bundle to deploy your microservice and micro frontends to Entando
- Iterate on your data model using the JHipster Domain Language (JDL)
# Notes
# Local vs. Entando Application Testing
This tutorial leverages the internal
client, which is configured in the microservice via the application.yml
. Client roles are manually created and assigned in Keycloak.
In Kubernetes, Entando will automatically create client roles per the bundle plugin definition (see the plugin definition for more information). These roles are created for the client specific to the microservice, e.g. pn-YOUR-SERVICE-ID-conference-ms
. The client name is injected as an environment variable into the plugin container, so the annotations noted above will work in both local and Kubernetes environments.
# Keycloak Client Options in an Entando Application
In this tutorial, the MFE authorization checks explicitly note the client ID, e.g. internal
. The following options modify the checks to work in an Entando Application:
Change the
application.yml
client ID undersecurity.oauth2.client.registration.oidc
to match the Kubernetes client ID.This is the most secure option and allows the MFE checks to work identically in both local and Kubernetes environments. However, you may not be able to use the same clientId, depending on how the microservice is deployed.
Broaden the MFE authorization check to look for a named role on any client.
This could result in overlap with roles created for other clients, but this is the most flexible option when using appropriately named roles (e.g. with a bundle or feature prefix like
conference-
inconference-admin
). It can be achieved via a helper function, e.g.api/helpers.js
, and results in a simpler role check:
// Add helper function
// Check if the authenticated user has the clientRole for any Keycloak clients
export const hasKeycloakClientRole = clientRole => {
if (getKeycloakToken()) {
const { resourceAccess } = window.entando.keycloak;
if (resourceAccess) {
for (const client in resourceAccess) {
// eslint-disable-line no-unused-vars
const roles = resourceAccess[client].roles;
if (roles && roles.includes(clientRole)) {
return true;
}
}
}
}
return false;
};
// Update the role check in ConferenceTableContainer.js using the new helper function
const isAdmin = hasKeycloakClientRole('conference-admin');
# Realm Roles versus Client Authorities
This tutorial utilizes authorities. In Keycloak, authorities are roles mapped to a user for a specific client. It is possible to assign higher-level Realm Roles directly to users, e.g. ROLE_ADMIN
, but this can result in collisions between applications using the same roles.
To implement Realm-assigned roles, the code above must be modified:
- In the backend, use the annotation
@Secured('ROLE_ADMIN)
or@PreAuthorize(hasRole('ROLE_ADMIN'))
- In the frontend, use
keycloak.hasRealmRole
instead ofkeycloak.hasResourceRole
See the Spring Security page (opens new window) for more examples.
# Troubleshooting
In both local and Kubernetes environments, the default Blueprint Javascript provides a global variable in the browser, e.g. window.entando.keycloak
. Examining this variable can help diagnose issues with assigned roles and authorities. In some cases, you may need to logout of Entando and reauthenticate for the latest role assignments to be applied.