BAEL-6620: RBAC Quarkus (#16416)
* BAEL-6620: RBAC quarkus * BAEL-6620: fix permissions * Code fix * Fix test name
This commit is contained in:
parent
0c1e41f3e8
commit
de699e2386
|
@ -0,0 +1,2 @@
|
||||||
|
## Relevant Articles
|
||||||
|
|
|
@ -0,0 +1,177 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"
|
||||||
|
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<groupId>com.baeldung.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-rbac</artifactId>
|
||||||
|
<version>1.0.0-SNAPSHOT</version>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>com.baeldung</groupId>
|
||||||
|
<artifactId>quarkus-modules</artifactId>
|
||||||
|
<version>1.0.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>${quarkus.platform.group-id}</groupId>
|
||||||
|
<artifactId>${quarkus.platform.artifact-id}</artifactId>
|
||||||
|
<version>${quarkus.platform.version}</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-jdbc-h2</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-hibernate-orm</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-hibernate-orm-panache</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-resteasy</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-smallrye-jwt-build</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-smallrye-jwt</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-resteasy-jackson</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-security-jpa</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-arc</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-junit5</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.rest-assured</groupId>
|
||||||
|
<artifactId>rest-assured</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-test-security</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-test-security-jwt</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>${quarkus.platform.group-id}</groupId>
|
||||||
|
<artifactId>quarkus-maven-plugin</artifactId>
|
||||||
|
<version>${quarkus.platform.version}</version>
|
||||||
|
<extensions>true</extensions>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<goals>
|
||||||
|
<goal>build</goal>
|
||||||
|
<goal>generate-code</goal>
|
||||||
|
<goal>generate-code-tests</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>${compiler-plugin.version}</version>
|
||||||
|
<configuration>
|
||||||
|
<compilerArgs>
|
||||||
|
<arg>-parameters</arg>
|
||||||
|
</compilerArgs>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<version>${surefire-plugin.version}</version>
|
||||||
|
<configuration>
|
||||||
|
<systemPropertyVariables>
|
||||||
|
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
|
||||||
|
<maven.home>${maven.home}</maven.home>
|
||||||
|
</systemPropertyVariables>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
<profiles>
|
||||||
|
<profile>
|
||||||
|
<id>native</id>
|
||||||
|
<activation>
|
||||||
|
<property>
|
||||||
|
<name>native</name>
|
||||||
|
</property>
|
||||||
|
</activation>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-failsafe-plugin</artifactId>
|
||||||
|
<version>${surefire-plugin.version}</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<goals>
|
||||||
|
<goal>integration-test</goal>
|
||||||
|
<goal>verify</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<systemPropertyVariables>
|
||||||
|
<native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
|
||||||
|
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
|
||||||
|
<maven.home>${maven.home}</maven.home>
|
||||||
|
</systemPropertyVariables>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
<properties>
|
||||||
|
<quarkus.package.type>native</quarkus.package.type>
|
||||||
|
</properties>
|
||||||
|
</profile>
|
||||||
|
</profiles>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<compiler-plugin.version>3.12.1</compiler-plugin.version>
|
||||||
|
<failsafe.useModulePath>false</failsafe.useModulePath>
|
||||||
|
<maven.compiler.release>17</maven.compiler.release>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||||
|
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
|
||||||
|
<quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
|
||||||
|
<quarkus.platform.version>3.9.3</quarkus.platform.version>
|
||||||
|
<surefire-plugin.version>3.0.0-M7</surefire-plugin.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
</project>
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.baeldung.quarkus.rbac.api;
|
||||||
|
|
||||||
|
import com.baeldung.quarkus.rbac.users.errors.DomainDataException;
|
||||||
|
import jakarta.ws.rs.core.Response;
|
||||||
|
import jakarta.ws.rs.ext.ExceptionMapper;
|
||||||
|
import jakarta.ws.rs.ext.Provider;
|
||||||
|
|
||||||
|
@Provider
|
||||||
|
public class ApiErrorHandler implements ExceptionMapper<DomainDataException> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response toResponse(final DomainDataException exception) {
|
||||||
|
return Response.status(Response.Status.BAD_REQUEST)
|
||||||
|
.entity(exception.getEntity() == null? exception.getEntity() : new Message(exception.getMessage()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package com.baeldung.quarkus.rbac.api;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
public record LoginDto(@NotNull String username, @NotNull String password) {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "LoginDto{" +
|
||||||
|
"username='" + username + '\'' +
|
||||||
|
", password='*********'" +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
package com.baeldung.quarkus.rbac.api;
|
||||||
|
|
||||||
|
public record Message(String message) { }
|
|
@ -0,0 +1,37 @@
|
||||||
|
package com.baeldung.quarkus.rbac.api;
|
||||||
|
|
||||||
|
import io.quarkus.security.PermissionsAllowed;
|
||||||
|
import io.quarkus.security.identity.SecurityIdentity;
|
||||||
|
import jakarta.ws.rs.Consumes;
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
|
||||||
|
@Path("/permission-based")
|
||||||
|
public class PermissionBasedController {
|
||||||
|
|
||||||
|
private final SecurityIdentity securityIdentity;
|
||||||
|
|
||||||
|
public PermissionBasedController(SecurityIdentity securityIdentity) {
|
||||||
|
this.securityIdentity = securityIdentity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/resource/version")
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@PermissionsAllowed("VIEW_ADMIN_DETAILS")
|
||||||
|
public String get() {
|
||||||
|
return "2.0.0";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Path("/resource/message")
|
||||||
|
@PermissionsAllowed(value = {"SEND_MESSAGE", "OPERATOR"}, inclusive = true)
|
||||||
|
public Message message() {
|
||||||
|
return new Message("Hello "+securityIdentity.getPrincipal().getName()+"!");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
package com.baeldung.quarkus.rbac.api;
|
||||||
|
|
||||||
|
import com.baeldung.quarkus.rbac.users.Role;
|
||||||
|
import com.baeldung.quarkus.rbac.users.UserService;
|
||||||
|
import io.quarkus.security.identity.SecurityIdentity;
|
||||||
|
import jakarta.annotation.security.PermitAll;
|
||||||
|
import jakarta.annotation.security.RolesAllowed;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.ws.rs.Consumes;
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.POST;
|
||||||
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.Produces;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import jakarta.ws.rs.core.Response;
|
||||||
|
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Path("/secured")
|
||||||
|
public class SecureResourceController {
|
||||||
|
|
||||||
|
private final UserService userService;
|
||||||
|
private final SecurityIdentity securityIdentity;
|
||||||
|
|
||||||
|
public SecureResourceController(UserService userService, SecurityIdentity securityIdentity) {
|
||||||
|
this.userService = userService;
|
||||||
|
this.securityIdentity = securityIdentity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/resource")
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@RolesAllowed({"VIEW_ADMIN_DETAILS"})
|
||||||
|
public String get() {
|
||||||
|
return "Hello world, here are some details about the admin!";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/resource/user")
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@RolesAllowed({"VIEW_USER_DETAILS"})
|
||||||
|
public Message getUser() {
|
||||||
|
return new Message("Hello "+securityIdentity.getPrincipal().getName()+"!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/resource")
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@RolesAllowed("${customer.send.message.permission:SEND_MESSAGE}")
|
||||||
|
public Response getUser(Message message) {
|
||||||
|
return Response.ok(message).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/user")
|
||||||
|
@RolesAllowed({"CREATE_USER"})
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public Response postUser(@Valid final UserDto userDto) {
|
||||||
|
|
||||||
|
final var user = userService.createUser(userDto);
|
||||||
|
|
||||||
|
final var roles = user.getRoles().stream().map(Role::getName).collect(Collectors.toSet());
|
||||||
|
|
||||||
|
return Response.ok(new UserResponse(user.getUsername(), roles)).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/login")
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@PermitAll
|
||||||
|
public Response login(@Valid final LoginDto loginDto) {
|
||||||
|
if (userService.checkUserCredentials(loginDto.username(), loginDto.password())) {
|
||||||
|
final var user = userService.findByUsername(loginDto.username());
|
||||||
|
final var token = userService.generateJwtToken(user);
|
||||||
|
return Response.ok().entity(new TokenResponse("Bearer " + token,"3600")).build();
|
||||||
|
} else {
|
||||||
|
return Response.status(Response.Status.UNAUTHORIZED).entity(new Message("Invalid credentials")).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
package com.baeldung.quarkus.rbac.api;
|
||||||
|
|
||||||
|
public record TokenResponse(String token, String expiresIn){
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package com.baeldung.quarkus.rbac.api;
|
||||||
|
|
||||||
|
import io.smallrye.common.constraint.NotNull;
|
||||||
|
import jakarta.validation.constraints.Email;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public record UserDto(@NotNull String username, @NotNull String password, @Size(min = 1) Set<String> roles, @Email String email) { }
|
|
@ -0,0 +1,6 @@
|
||||||
|
package com.baeldung.quarkus.rbac.api;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public record UserResponse(String username, Set<String> roles) { }
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
package com.baeldung.quarkus.rbac.users;
|
||||||
|
|
||||||
|
public enum Permission {
|
||||||
|
|
||||||
|
VIEW_ADMIN_DETAILS,
|
||||||
|
VIEW_USER_DETAILS,
|
||||||
|
SEND_MESSAGE,
|
||||||
|
CREATE_USER
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
package com.baeldung.quarkus.rbac.users;
|
||||||
|
|
||||||
|
import jakarta.persistence.AttributeConverter;
|
||||||
|
import jakarta.persistence.Converter;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Converter
|
||||||
|
public class PermissionConverter implements AttributeConverter<Set<Permission>, String> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String convertToDatabaseColumn(Set<Permission> attribute) {
|
||||||
|
if (attribute == null || attribute.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return attribute.stream()
|
||||||
|
.map(Permission::name)
|
||||||
|
.collect(Collectors.joining(","));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<Permission> convertToEntityAttribute(String dbData) {
|
||||||
|
if (dbData == null || dbData.isEmpty()) {
|
||||||
|
return Set.of();
|
||||||
|
}
|
||||||
|
return Arrays.stream(dbData.split(","))
|
||||||
|
.map(String::trim)
|
||||||
|
.map(Permission::valueOf)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
package com.baeldung.quarkus.rbac.users;
|
||||||
|
|
||||||
|
import io.quarkus.security.jpa.Roles;
|
||||||
|
import jakarta.persistence.Convert;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "roles")
|
||||||
|
public class Role {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Roles
|
||||||
|
@Convert(converter = PermissionConverter.class)
|
||||||
|
private Set<Permission> permissions = new HashSet<>();
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<Permission> getPermissions() {
|
||||||
|
return permissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPermissions(Set<Permission> permissions) {
|
||||||
|
this.permissions = permissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
package com.baeldung.quarkus.rbac.users;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.FetchType;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.JoinTable;
|
||||||
|
import jakarta.persistence.ManyToMany;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "users")
|
||||||
|
public class User {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(unique = true)
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
@Column(unique = true)
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
@ManyToMany(fetch = FetchType.LAZY)
|
||||||
|
@JoinTable(name = "user_roles",
|
||||||
|
joinColumns = @JoinColumn(name = "user_id"),
|
||||||
|
inverseJoinColumns = @JoinColumn(name = "role_name"))
|
||||||
|
private Set<Role> roles = new HashSet<>();
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUsername(String username) {
|
||||||
|
this.username = username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPassword() {
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPassword(String password) {
|
||||||
|
this.password = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEmail() {
|
||||||
|
return email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEmail(String email) {
|
||||||
|
this.email = email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<Role> getRoles() {
|
||||||
|
return roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRoles(Set<Role> roles) {
|
||||||
|
this.roles = roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addRole(Role role) {
|
||||||
|
roles.add(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeRole(Role role) {
|
||||||
|
roles.remove(role);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
package com.baeldung.quarkus.rbac.users;
|
||||||
|
|
||||||
|
import io.quarkus.hibernate.orm.panache.PanacheRepository;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
class UserRepository implements PanacheRepository<User> {
|
||||||
|
|
||||||
|
public List<Role> findRoles(final Collection<String> roles) {
|
||||||
|
if (roles == null || roles.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
return find("SELECT r FROM Role r WHERE r.name in ?1", roles).project(Role.class).list();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
package com.baeldung.quarkus.rbac.users;
|
||||||
|
|
||||||
|
import com.baeldung.quarkus.rbac.api.UserDto;
|
||||||
|
import com.baeldung.quarkus.rbac.users.errors.EntityNotFoundException;
|
||||||
|
import com.baeldung.quarkus.rbac.users.errors.InvalidRolesProvidedException;
|
||||||
|
import io.quarkus.elytron.security.common.BcryptUtil;
|
||||||
|
import io.smallrye.jwt.build.Jwt;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||||
|
import org.eclipse.microprofile.jwt.Claims;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@ApplicationScoped
|
||||||
|
public class UserService {
|
||||||
|
|
||||||
|
private final UserRepository repository;
|
||||||
|
private final String issuer;
|
||||||
|
|
||||||
|
public UserService(final UserRepository repository,
|
||||||
|
final @ConfigProperty(name = "mp.jwt.verify.issuer") String issuer) {
|
||||||
|
this.repository = repository;
|
||||||
|
this.issuer = issuer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String generateJwtToken(final User user) {
|
||||||
|
|
||||||
|
final Set<String> permissions = user.getRoles()
|
||||||
|
.stream()
|
||||||
|
.flatMap(role -> role.getPermissions().stream())
|
||||||
|
.map(Permission::name)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
return Jwt.issuer(issuer)
|
||||||
|
.upn(user.getUsername())
|
||||||
|
.groups(permissions)
|
||||||
|
.expiresIn(3600)
|
||||||
|
.claim(Claims.email_verified.name(), user.getEmail())
|
||||||
|
.sign();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public User createUser(final UserDto userDto) {
|
||||||
|
|
||||||
|
final var roles = repository.findRoles(userDto.roles());
|
||||||
|
|
||||||
|
if (roles.size() != userDto.roles().size()) {
|
||||||
|
throw new InvalidRolesProvidedException("Unknown role provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
final var user = new User();
|
||||||
|
|
||||||
|
user.setUsername(userDto.username());
|
||||||
|
user.setPassword(BcryptUtil.bcryptHash(userDto.password()));
|
||||||
|
user.setRoles(new HashSet<>(roles));
|
||||||
|
user.setEmail(userDto.email());
|
||||||
|
|
||||||
|
repository.persist(user);
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean checkUserCredentials(String username, String password) {
|
||||||
|
final User user = findByUsername(username);
|
||||||
|
return BcryptUtil.matches(password, user.getPassword());
|
||||||
|
}
|
||||||
|
|
||||||
|
public User findByUsername(String username) {
|
||||||
|
return repository.find("username", username).firstResultOptional()
|
||||||
|
.orElseThrow(() -> new EntityNotFoundException(username));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.baeldung.quarkus.rbac.users.errors;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
public abstract class DomainDataException extends RuntimeException {
|
||||||
|
|
||||||
|
private final Serializable entity;
|
||||||
|
|
||||||
|
protected DomainDataException(final String message, final Serializable entity) {
|
||||||
|
super(message);
|
||||||
|
this.entity = entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Serializable getEntity() {
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package com.baeldung.quarkus.rbac.users.errors;
|
||||||
|
|
||||||
|
public class EntityNotFoundException extends RuntimeException {
|
||||||
|
|
||||||
|
public EntityNotFoundException(String id) {
|
||||||
|
super("Entity with id " + id + " not found");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package com.baeldung.quarkus.rbac.users.errors;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
public class InvalidRolesProvidedException extends DomainDataException {
|
||||||
|
|
||||||
|
public InvalidRolesProvidedException(String message, Serializable entity) {
|
||||||
|
super(message, entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public InvalidRolesProvidedException(String message) {
|
||||||
|
super(message, null);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
mp.jwt.verify.publickey.location=publicKey.pem
|
||||||
|
quarkus.native.resources.includes=publicKey.pem
|
||||||
|
mp.jwt.verify.issuer=my-issuer
|
||||||
|
smallrye.jwt.sign.key.location=privateKey.pem
|
||||||
|
|
||||||
|
quarkus.datasource.db-kind=h2
|
||||||
|
quarkus.datasource.jdbc.url=jdbc:h2:mem:testdb_;DB_CLOSE_DELAY=-1;MODE=MYSQL;DB_CLOSE_ON_EXIT=TRUE;DATABASE_TO_LOWER=TRUE
|
||||||
|
quarkus.hibernate-orm.dialect=org.hibernate.dialect.MySQLDialect
|
||||||
|
quarkus.hibernate-orm.database.generation=drop-and-create
|
||||||
|
quarkus.hibernate-orm.log.sql=true
|
||||||
|
quarkus.hibernate-orm.sql-load-script=import.sql
|
||||||
|
|
||||||
|
quarkus.http.auth.policy.role-policy1.permissions.VIEW_ADMIN_DETAILS=VIEW_ADMIN_DETAILS
|
||||||
|
quarkus.http.auth.policy.role-policy1.permissions.VIEW_USER_DETAILS=VIEW_USER_DETAILS
|
||||||
|
quarkus.http.auth.policy.role-policy1.permissions.SEND_MESSAGE=SEND_MESSAGE
|
||||||
|
quarkus.http.auth.policy.role-policy1.permissions.CREATE_USER=CREATE_USER
|
||||||
|
quarkus.http.auth.policy.role-policy1.permissions.OPERATOR=OPERATOR
|
||||||
|
quarkus.http.auth.permission.roles1.paths=/permission-based/*
|
||||||
|
quarkus.http.auth.permission.roles1.policy=role-policy1
|
|
@ -0,0 +1,11 @@
|
||||||
|
insert into roles (name, permissions) values ('ADMIN', 'VIEW_ADMIN_DETAILS,CREATE_USER,SEND_MESSAGE,VIEW_USER_DETAILS');
|
||||||
|
insert into roles (name, permissions) values ('USER', 'VIEW_USER_DETAILS,SEND_MESSAGE');
|
||||||
|
insert into roles (name, permissions) values ('GUEST', 'SEND_MESSAGE');
|
||||||
|
|
||||||
|
insert into users (id, username, password, email) values (1, 'admin', '$2a$10$sWfRL1ruggfeebSfeMnToOeeuQTzSFY.khIlS/dzWY6qYOekisccS', 'admin@test.io');
|
||||||
|
insert into users (id, username, password, email) values (2, 'user', '$2a$16$6AZvwlL1PCJ7fgoNVBlezOnoB6WkZlmj6mvQPP5/0uWyso8nVOdXm', 'user@test.io');
|
||||||
|
insert into users (id, username, password, email) values (3, 'guest', '$2a$16$Sd0wA6le90dUTe3OAUiSZe.FmPL96XLvRYUUbhitF3.dmgF/dLgFm', 'guest@test.io');
|
||||||
|
|
||||||
|
insert into user_roles (user_id, role_name) values (1, 'ADMIN');
|
||||||
|
insert into user_roles (user_id, role_name) values (2, 'USER');
|
||||||
|
insert into user_roles (user_id, role_name) values (3, 'GUEST');
|
|
@ -0,0 +1,28 @@
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCWK8UjyoHgPTLa
|
||||||
|
PLQJ8SoXLLjpHSjtLxMqmzHnFscqhTVVaDpCRCb6e3Ii/WniQTWw8RA7vf4djz4H
|
||||||
|
OzvlfBFNgvUGZHXDwnmGaNVaNzpHYFMEYBhE8VGGiveSkzqeLZI+Y02G6sQAfDtN
|
||||||
|
qqzM/l5QX8X34oQFaTBW1r49nftvCpITiwJvWyhkWtXP9RP8sXi1im5Vi3dhupOh
|
||||||
|
nelk5n0BfajUYIbfHA6ORzjHRbt7NtBl0L2J+0/FUdHyKs6KMlFGNw8O0Dq88qnM
|
||||||
|
uXoLJiewhg9332W3DFMeOveel+//cvDnRsCRtPgd4sXFPHh+UShkso7+DRsChXa6
|
||||||
|
oGGQD3GdAgMBAAECggEAAjfTSZwMHwvIXIDZB+yP+pemg4ryt84iMlbofclQV8hv
|
||||||
|
6TsI4UGwcbKxFOM5VSYxbNOisb80qasb929gixsyBjsQ8284bhPJR7r0q8h1C+jY
|
||||||
|
URA6S4pk8d/LmFakXwG9Tz6YPo3pJziuh48lzkFTk0xW2Dp4SLwtAptZY/+ZXyJ6
|
||||||
|
96QXDrZKSSM99Jh9s7a0ST66WoxSS0UC51ak+Keb0KJ1jz4bIJ2C3r4rYlSu4hHB
|
||||||
|
Y73GfkWORtQuyUDa9yDOem0/z0nr6pp+pBSXPLHADsqvZiIhxD/O0Xk5I6/zVHB3
|
||||||
|
zuoQqLERk0WvA8FXz2o8AYwcQRY2g30eX9kU4uDQAQKBgQDmf7KGImUGitsEPepF
|
||||||
|
KH5yLWYWqghHx6wfV+fdbBxoqn9WlwcQ7JbynIiVx8MX8/1lLCCe8v41ypu/eLtP
|
||||||
|
iY1ev2IKdrUStvYRSsFigRkuPHUo1ajsGHQd+ucTDf58mn7kRLW1JGMeGxo/t32B
|
||||||
|
m96Af6AiPWPEJuVfgGV0iwg+HQKBgQCmyPzL9M2rhYZn1AozRUguvlpmJHU2DpqS
|
||||||
|
34Q+7x2Ghf7MgBUhqE0t3FAOxEC7IYBwHmeYOvFR8ZkVRKNF4gbnF9RtLdz0DMEG
|
||||||
|
5qsMnvJUSQbNB1yVjUCnDAtElqiFRlQ/k0LgYkjKDY7LfciZl9uJRl0OSYeX/qG2
|
||||||
|
tRW09tOpgQKBgBSGkpM3RN/MRayfBtmZvYjVWh3yjkI2GbHA1jj1g6IebLB9SnfL
|
||||||
|
WbXJErCj1U+wvoPf5hfBc7m+jRgD3Eo86YXibQyZfY5pFIh9q7Ll5CQl5hj4zc4Y
|
||||||
|
b16sFR+xQ1Q9Pcd+BuBWmSz5JOE/qcF869dthgkGhnfVLt/OQzqZluZRAoGAXQ09
|
||||||
|
nT0TkmKIvlza5Af/YbTqEpq8mlBDhTYXPlWCD4+qvMWpBII1rSSBtftgcgca9XLB
|
||||||
|
MXmRMbqtQeRtg4u7dishZVh1MeP7vbHsNLppUQT9Ol6lFPsd2xUpJDc6BkFat62d
|
||||||
|
Xjr3iWNPC9E9nhPPdCNBv7reX7q81obpeXFMXgECgYEAmk2Qlus3OV0tfoNRqNpe
|
||||||
|
Mb0teduf2+h3xaI1XDIzPVtZF35ELY/RkAHlmWRT4PCdR0zXDidE67L6XdJyecSt
|
||||||
|
FdOUH8z5qUraVVebRFvJqf/oGsXc4+ex1ZKUTbY0wqY1y9E39yvB3MaTmZFuuqk8
|
||||||
|
f3cg+fr8aou7pr9SHhJlZCU=
|
||||||
|
-----END PRIVATE KEY-----
|
|
@ -0,0 +1,9 @@
|
||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlivFI8qB4D0y2jy0CfEq
|
||||||
|
Fyy46R0o7S8TKpsx5xbHKoU1VWg6QkQm+ntyIv1p4kE1sPEQO73+HY8+Bzs75XwR
|
||||||
|
TYL1BmR1w8J5hmjVWjc6R2BTBGAYRPFRhor3kpM6ni2SPmNNhurEAHw7TaqszP5e
|
||||||
|
UF/F9+KEBWkwVta+PZ37bwqSE4sCb1soZFrVz/UT/LF4tYpuVYt3YbqToZ3pZOZ9
|
||||||
|
AX2o1GCG3xwOjkc4x0W7ezbQZdC9iftPxVHR8irOijJRRjcPDtA6vPKpzLl6CyYn
|
||||||
|
sIYPd99ltwxTHjr3npfv/3Lw50bAkbT4HeLFxTx4flEoZLKO/g0bAoV2uqBhkA9x
|
||||||
|
nQIDAQAB
|
||||||
|
-----END PUBLIC KEY-----
|
|
@ -0,0 +1,57 @@
|
||||||
|
package com.baeldung.quarkus;
|
||||||
|
|
||||||
|
import io.quarkus.test.junit.QuarkusTest;
|
||||||
|
import io.quarkus.test.security.TestSecurity;
|
||||||
|
import io.quarkus.test.security.jwt.Claim;
|
||||||
|
import io.quarkus.test.security.jwt.JwtSecurity;
|
||||||
|
import io.restassured.http.ContentType;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static io.restassured.RestAssured.given;
|
||||||
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
|
|
||||||
|
@QuarkusTest
|
||||||
|
class PermissionBasedControllerIntegrationTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@TestSecurity(user = "admin", roles = "VIEW_ADMIN_DETAILS")
|
||||||
|
@JwtSecurity(claims = {
|
||||||
|
@Claim(key = "email", value = "admin@test.io")
|
||||||
|
})
|
||||||
|
void givenSecureVersionApi_whenUserIsAuthenticated_thenShouldReturnVersion() {
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.get("/permission-based/resource/version")
|
||||||
|
.then()
|
||||||
|
.statusCode(200)
|
||||||
|
.body(equalTo("2.0.0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@TestSecurity(user = "user", roles = "SEND_MESSAGE")
|
||||||
|
@JwtSecurity(claims = {
|
||||||
|
@Claim(key = "email", value = "user@test.io")
|
||||||
|
})
|
||||||
|
void givenSecureMessageApi_whenUserOnlyHasOnePermission_thenShouldNotAllowRequest() {
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.get("/permission-based/resource/message")
|
||||||
|
.then()
|
||||||
|
.statusCode(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@TestSecurity(user = "new-operator", roles = {"SEND_MESSAGE", "OPERATOR"})
|
||||||
|
@JwtSecurity(claims = {
|
||||||
|
@Claim(key = "email", value = "operator@test.io")
|
||||||
|
})
|
||||||
|
void givenSecureMessageApi_whenUserOnlyHasBothPermissions_thenShouldAllowRequest() {
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.get("/permission-based/resource/message")
|
||||||
|
.then()
|
||||||
|
.statusCode(200)
|
||||||
|
.body("message", equalTo("Hello new-operator!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
package com.baeldung.quarkus;
|
||||||
|
|
||||||
|
import com.baeldung.quarkus.rbac.api.LoginDto;
|
||||||
|
import com.baeldung.quarkus.rbac.api.Message;
|
||||||
|
import io.quarkus.test.junit.QuarkusTest;
|
||||||
|
import io.quarkus.test.security.TestSecurity;
|
||||||
|
import io.quarkus.test.security.jwt.Claim;
|
||||||
|
import io.quarkus.test.security.jwt.JwtSecurity;
|
||||||
|
import io.restassured.http.ContentType;
|
||||||
|
import io.restassured.mapper.ObjectMapperType;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static io.restassured.RestAssured.given;
|
||||||
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
|
import static org.hamcrest.Matchers.notNullValue;
|
||||||
|
|
||||||
|
@QuarkusTest
|
||||||
|
class SecureResourceControllerIntegrationTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void givenSecureLoginApi_whenAdminLogsIn_thenShouldReturnOK() {
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body(new LoginDto("admin", "admin"), ObjectMapperType.JACKSON_2)
|
||||||
|
.post("/secured/login")
|
||||||
|
.then()
|
||||||
|
.statusCode(200)
|
||||||
|
.body("token", notNullValue())
|
||||||
|
.body("expiresIn", equalTo("3600"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@TestSecurity(user = "user", roles = "VIEW_USER_DETAILS")
|
||||||
|
@JwtSecurity(claims = {
|
||||||
|
@Claim(key = "email", value = "user@test.io")
|
||||||
|
})
|
||||||
|
void givenSecureUserApi_whenUserIsAuthenticated_thenShouldReturnCustomMessage() {
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.get("/secured/resource/user")
|
||||||
|
.then()
|
||||||
|
.statusCode(200)
|
||||||
|
.body("message", equalTo("Hello user!"));;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@TestSecurity(user = "user", roles = "VIEW_USER_DETAILS")
|
||||||
|
@JwtSecurity(claims = {
|
||||||
|
@Claim(key = "email", value = "user@test.io")
|
||||||
|
})
|
||||||
|
void givenSecureAdminApi_whenUserTriesToAccessAdminApi_thenShouldNotAllowRequest() {
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.get("/secured/resource")
|
||||||
|
.then()
|
||||||
|
.statusCode(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@TestSecurity(user = "admin", roles = "VIEW_ADMIN_DETAILS")
|
||||||
|
@JwtSecurity(claims = {
|
||||||
|
@Claim(key = "email", value = "admin@test.io")
|
||||||
|
})
|
||||||
|
void givenSecureAdminApi_whenAdminTriesAccessAdminApi_thenShouldAllowRequest() {
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.get("/secured/resource")
|
||||||
|
.then()
|
||||||
|
.statusCode(200)
|
||||||
|
.body(equalTo("Hello world, here are some details about the admin!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@TestSecurity(user = "guest", roles = "SEND_MESSAGE")
|
||||||
|
@JwtSecurity(claims = {
|
||||||
|
@Claim(key = "email", value = "guest@test.io")
|
||||||
|
})
|
||||||
|
void givenSecureGuestApi_whenCallAsGuess_thenShouldReturnOk() {
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body(new Message("Hello Friend!"), ObjectMapperType.JACKSON_2)
|
||||||
|
.post("/secured/resource")
|
||||||
|
.then()
|
||||||
|
.statusCode(200)
|
||||||
|
.body("message", equalTo("Hello Friend!"));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue