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

ENDOC-578 Update the RBAC tutorial for Entando 7.1 #587

Merged
merged 2 commits into from
Oct 6, 2022
Merged
Changes from 1 commit
Commits
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
121 changes: 50 additions & 71 deletions vuepress/docs/next/tutorials/create/ms/add-access-controls.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ The basic security setup for a blueprint-generated application allows any authen

The list of Conferences must be visible to only the `conference-user` and `conference-admin` user roles.

1. Go to the `src/main/java/com/YOUR-ORG/YOUR-NAME/web/rest` directory
1. Go to `microservices/conference-ms/src/main/java/com/YOUR-ORG/YOUR-APP-NAME/web/rest`
2. Open `ConferenceResource.java`
3. Add the following to the list of imports:
```java
Expand All @@ -30,80 +30,43 @@ The list of Conferences must be visible to only the `conference-user` and `confe
```
This confines 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.

> Note: In local testing, the default client is `internal`. Refer to the [Spring Security documentation](https://spring.io/projects/spring-security) for more information.

We also need to modify the blueprint JWT handling to deal with a recent change to the Spring libraries.

5. Edit `src/main/java/com/mycompany/myapp/config/SecurityConfiguration.java` and make two changes.
* Add this code after the other @Value fields
``` java
@Value("${spring.security.oauth2.client.registration.oidc.client-id}")
private String clientId;
```
* Modify the following call in the `authenticationConverter` method to provide the clientId field
``` java
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new JwtGrantedAuthorityConverter(clientId));
```
6. Now modify
`src/main/java/com/mycompany/myapp/security/oauth2/JwtGrantedAuthorityConverter.java` to accept the clientId. Three changes are required.
* Remove the @Component annotation on the class definition
```java{1}
@Component
public class JwtGrantedAuthorityConverter implements Converter<Jwt, Collection<GrantedAuthority>> {
```
* Remove the @Value annotation on the clientId field
```java{1}
@Value("${spring.security.oauth2.client.registration.oidc.client-id}")
private String clientId;
```
* Modify the constructor to accept the clientId
```java
public JwtGrantedAuthorityConverter(String clientId) {
this.clientId = clientId;
}
```

### Step 2: Run your project in a local developer environment
The following commands must be run from your project directory. They leverage the [ent CLI](../../../docs/getting-started/entando-cli.md).
The following commands must be run from your bundle project directory. They leverage the [ent CLI](../../../docs/getting-started/entando-cli.md).

> Note: Refer to the [Run Blueprint-generated Microservices and Micro Frontends in Dev Mode tutorial](./run-local.md) for details.
> Note: Refer to the [Run Blueprint-generated Microservices and Micro Frontends in Dev Mode tutorial](./run-local.md) for more details.

1. Start up your Keycloak instance
``` sh
ent prj ext-keycloak start
ent bundle svc start keycloak
```
2. Start the microservice in another shell
``` sh
ent prj be-test-run
ent bundle run conference-ms
```
3. Start the tableWidget MFE in a third shell
3. Start the conference-table MFE in a third shell
``` sh
ent prj fe-test-run
ent bundle run conference-table
```
4. When prompted to select a widget to run, choose the option corresponding to the tableWidget, e.g. `ui/widgets/conference/tableWidget`

### Step 3: Access the tableWidget MFE
### Step 3: Access the conference-table MFE

1. In your browser, go to <http://localhost:3000>. This is typically the location of the tableWidget MFE.
2. Access the tableWidget MFE with the default credentials of `username: admin`, `password: admin`
1. In your browser, go to <http://localhost:3000>.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: remove period for consistency

2. 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 has not yet been granted the new role.
> 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 has not yet been granted the new role.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

backtick admin in last line


### Step 4: Login to Keycloak

1. Go to <http://localhost:9080>
2. Login using the the default credentials of `username: admin`, `password: admin`
2. Login using the default credentials of `username: admin`, `password: admin`

### Step 5: Create the `conference-user` and `conference-admin` roles

Add the `conference-user` and `conference-admin` roles to the `internal` client.

1. Go to `Clients` → `internal` → `Roles`
2. Click `Add Role`
3. Fill in the `Role Name` with `conference-admin`
3. Fill in the `Role Name` with `conference-user`
4. Click `Save`
5. Repeat these steps to create the `conference-user` role
5. Repeat these steps to create the `conference-admin` role

> Note: The `internal` client is configured by default in the Spring Boot `application.yml`.

Expand All @@ -114,13 +77,13 @@ To grant access to the `getAllConferences` API:
1. Go to `Users` → `View all users` → `admin` → `Role Mappings`
2. Select `internal` for the `Client Roles`
3. Move `conference-user` from `Available Roles` to `Assigned Roles`
4. Return to the MFE to confirm you see the full list of Conferences
4. 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:

1. Go to the `src/main/java/com/YOUR-ORG/YOUR-NAME/web/rest` directory
1. Go to the `src/main/java/com/YOUR-ORG/YOUR-APP-NAME/web/rest` directory
2. Open `ConferenceResource.java`
3. Modify the `deleteConference` method by preceding it with the following annotation:
```java{1}
Expand All @@ -130,9 +93,9 @@ The `conference-admin` role should grant a user permission to delete Conferences

To verify that a user without the `conference-admin` role is unable to call the delete API:

1. Restart the microservice. By default this includes rebuilding any changed source files.
2. Once the microservice is available, return to the MFE and try deleting one of the Conferences in the list
3. Verify that attempting to delete via the UI generates a `403 error` in the browser console and an error in the service logs similar to the following:
1. Restart the microservice. By default, this includes rebuilding any changed source files.
2. Return to the MFE and try deleting one of the Conferences in the list
3. 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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"conference" is capitalized in the step above, which is contextually similar

```
WARN 3208 --- [ XNIO-1 task-3] o.z.problem.spring.common.AdviceTraits : Forbidden: Access is denied
```
Expand All @@ -141,7 +104,7 @@ WARN 3208 --- [ XNIO-1 task-3] o.z.problem.spring.common.AdviceTraits : Forbi

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.

1. Go to the `ui/widgets/conference/tableWidget/src/components` directory
1. Go to the `microfrontends/conference-table/src/components` directory
2. Open `ConferenceTableContainer.js`
3. Replace the `onDelete` logic with an additional user permission:
```javascript
Expand All @@ -164,32 +127,39 @@ Promote the admin user to a full `conference-admin` to reinstate the ability to
5. Confirm the delete icon is visible
6. Confirm a Conference can be successfully deleted from the list

## Notes
### 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.
### Step 10. Configure the roles in `entando.json`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prefer this reference to entando.json which is not prefaced by "the"; minor but technically correct to not use "the"

Entando can automatically add roles to your client (see [the notes below](#notes) for different client options) when your microservice is deployed.

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 of `keycloak.hasResourceRole`
1. Modify the `entando.json` by adding the following line to the `microservices/conference-ms`:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove "the" :)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

think it's correct either way, with or without the

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

```json
"roles": ["conference-admin","conference-user"]
```

See the [Spring Security page](https://www.baeldung.com/spring-security-check-user-role) for more examples.
## 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](./run-local.md)
- [Build and publish a project bundle](../pb/publish-project-bundle.md) to deploy your microservice and micro frontends to Entando
- [Iterate on your data model](./update-data-model.md) using the JHipster Domain Language (JDL)

## Notes

### Local vs. Kubernetes Testing
### 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](../../../docs/curate/bundle-details.md) for more information). These roles are created for the client specific to the microservice, e.g. `<docker username>-conference-server`. 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.
In Kubernetes, Entando will automatically create client roles per the bundle plugin definition (see the [plugin definition](../../../docs/curate/bundle-details.md) 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.

#### Modify Security Checks for Kubernetes
#### Keycloak Client options in an Entando Application
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Option (capitalized to match style of other headers)


In this tutorial, the MFE authorization checks explicitly note the client ID, e.g. `internal`. The following options modify the checks to work in Kubernetes:
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:

1) Change the `application.yml` client ID under `security.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 be able to use the same clientId, depending on how the microservice is deployed.
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.

2) Broaden the MFE authorization check to look for a named role on any client.

This could result in overlap with other clients, but this is the most flexible option when using appropriately named roles (e.g. with a bundle or feature prefix like `conference-` in `conference-admin`). It can be achieved via a helper function, e.g. `api/helpers.js`, and results in a simpler role check:
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-` in `conference-admin`). It can be achieved via a helper function, e.g. `api/helpers.js`, and results in a simpler role check:
```javascript
// Add helper function
// Check if the authenticated user has the clientRole for any Keycloak clients
Expand All @@ -209,9 +179,18 @@ export const hasKeycloakClientRole = clientRole => {
return false;
};

// Perform role check
// 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 of `keycloak.hasResourceRole`

See the [Spring Security page](https://www.baeldung.com/spring-security-check-user-role) 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.