diff --git a/quarkus-modules/quarkus-rbac/README.md b/quarkus-modules/quarkus-rbac/README.md new file mode 100644 index 0000000000..512b64d9b2 --- /dev/null +++ b/quarkus-modules/quarkus-rbac/README.md @@ -0,0 +1,2 @@ +## Relevant Articles + diff --git a/quarkus-modules/quarkus-rbac/pom.xml b/quarkus-modules/quarkus-rbac/pom.xml new file mode 100644 index 0000000000..28d779b1b0 --- /dev/null +++ b/quarkus-modules/quarkus-rbac/pom.xml @@ -0,0 +1,177 @@ + + + 4.0.0 + com.baeldung.quarkus + quarkus-rbac + 1.0.0-SNAPSHOT + + + com.baeldung + quarkus-modules + 1.0.0-SNAPSHOT + + + + + + + ${quarkus.platform.group-id} + ${quarkus.platform.artifact-id} + ${quarkus.platform.version} + pom + import + + + + + + + io.quarkus + quarkus-jdbc-h2 + + + io.quarkus + quarkus-hibernate-orm + + + io.quarkus + quarkus-hibernate-orm-panache + + + io.quarkus + quarkus-resteasy + + + io.quarkus + quarkus-smallrye-jwt-build + + + io.quarkus + quarkus-smallrye-jwt + + + io.quarkus + quarkus-resteasy-jackson + + + io.quarkus + quarkus-security-jpa + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + io.quarkus + quarkus-test-security + test + + + io.quarkus + quarkus-test-security-jwt + test + + + + + + + ${quarkus.platform.group-id} + quarkus-maven-plugin + ${quarkus.platform.version} + true + + + + build + generate-code + generate-code-tests + + + + + + maven-compiler-plugin + ${compiler-plugin.version} + + + -parameters + + + + + maven-surefire-plugin + ${surefire-plugin.version} + + + org.jboss.logmanager.LogManager + ${maven.home} + + + + + + + + + native + + + native + + + + + + maven-failsafe-plugin + ${surefire-plugin.version} + + + + integration-test + verify + + + + ${project.build.directory}/${project.build.finalName}-runner + org.jboss.logmanager.LogManager + ${maven.home} + + + + + + + + + native + + + + + + 3.12.1 + false + 17 + UTF-8 + UTF-8 + quarkus-bom + io.quarkus.platform + 3.9.3 + 3.0.0-M7 + + + diff --git a/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/api/ApiErrorHandler.java b/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/api/ApiErrorHandler.java new file mode 100644 index 0000000000..dc48a01e64 --- /dev/null +++ b/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/api/ApiErrorHandler.java @@ -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 { + + @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(); + } +} diff --git a/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/api/LoginDto.java b/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/api/LoginDto.java new file mode 100644 index 0000000000..7765994791 --- /dev/null +++ b/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/api/LoginDto.java @@ -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='*********'" + + '}'; + } +} diff --git a/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/api/Message.java b/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/api/Message.java new file mode 100644 index 0000000000..51bf02dc30 --- /dev/null +++ b/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/api/Message.java @@ -0,0 +1,3 @@ +package com.baeldung.quarkus.rbac.api; + +public record Message(String message) { } diff --git a/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/api/PermissionBasedController.java b/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/api/PermissionBasedController.java new file mode 100644 index 0000000000..ece375d4b8 --- /dev/null +++ b/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/api/PermissionBasedController.java @@ -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()+"!"); + } +} diff --git a/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/api/SecureResourceController.java b/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/api/SecureResourceController.java new file mode 100644 index 0000000000..81d24d22fc --- /dev/null +++ b/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/api/SecureResourceController.java @@ -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(); + } + } +} diff --git a/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/api/TokenResponse.java b/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/api/TokenResponse.java new file mode 100644 index 0000000000..8a427b2e2a --- /dev/null +++ b/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/api/TokenResponse.java @@ -0,0 +1,4 @@ +package com.baeldung.quarkus.rbac.api; + +public record TokenResponse(String token, String expiresIn){ +} diff --git a/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/api/UserDto.java b/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/api/UserDto.java new file mode 100644 index 0000000000..a8f6302712 --- /dev/null +++ b/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/api/UserDto.java @@ -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 roles, @Email String email) { } diff --git a/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/api/UserResponse.java b/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/api/UserResponse.java new file mode 100644 index 0000000000..2f10be8d36 --- /dev/null +++ b/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/api/UserResponse.java @@ -0,0 +1,6 @@ +package com.baeldung.quarkus.rbac.api; + +import java.util.Set; + +public record UserResponse(String username, Set roles) { } + diff --git a/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/users/Permission.java b/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/users/Permission.java new file mode 100644 index 0000000000..9127443685 --- /dev/null +++ b/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/users/Permission.java @@ -0,0 +1,10 @@ +package com.baeldung.quarkus.rbac.users; + +public enum Permission { + + VIEW_ADMIN_DETAILS, + VIEW_USER_DETAILS, + SEND_MESSAGE, + CREATE_USER + +} diff --git a/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/users/PermissionConverter.java b/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/users/PermissionConverter.java new file mode 100644 index 0000000000..b86a84accb --- /dev/null +++ b/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/users/PermissionConverter.java @@ -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, String> { + + @Override + public String convertToDatabaseColumn(Set attribute) { + if (attribute == null || attribute.isEmpty()) { + return null; + } + return attribute.stream() + .map(Permission::name) + .collect(Collectors.joining(",")); + } + + @Override + public Set 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()); + } +} diff --git a/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/users/Role.java b/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/users/Role.java new file mode 100644 index 0000000000..4177f695ae --- /dev/null +++ b/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/users/Role.java @@ -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 permissions = new HashSet<>(); + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Set getPermissions() { + return permissions; + } + + public void setPermissions(Set permissions) { + this.permissions = permissions; + } + +} diff --git a/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/users/User.java b/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/users/User.java new file mode 100644 index 0000000000..2eff427177 --- /dev/null +++ b/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/users/User.java @@ -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 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 getRoles() { + return roles; + } + + public void setRoles(Set roles) { + this.roles = roles; + } + + public void addRole(Role role) { + roles.add(role); + } + + public void removeRole(Role role) { + roles.remove(role); + } +} \ No newline at end of file diff --git a/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/users/UserRepository.java b/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/users/UserRepository.java new file mode 100644 index 0000000000..2802b50ba8 --- /dev/null +++ b/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/users/UserRepository.java @@ -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 { + + public List findRoles(final Collection 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(); + } +} diff --git a/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/users/UserService.java b/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/users/UserService.java new file mode 100644 index 0000000000..55f44df486 --- /dev/null +++ b/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/users/UserService.java @@ -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 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)); + } + +} diff --git a/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/users/errors/DomainDataException.java b/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/users/errors/DomainDataException.java new file mode 100644 index 0000000000..caad071e35 --- /dev/null +++ b/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/users/errors/DomainDataException.java @@ -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; + } +} diff --git a/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/users/errors/EntityNotFoundException.java b/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/users/errors/EntityNotFoundException.java new file mode 100644 index 0000000000..18e6f28f7c --- /dev/null +++ b/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/users/errors/EntityNotFoundException.java @@ -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"); + } +} diff --git a/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/users/errors/InvalidRolesProvidedException.java b/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/users/errors/InvalidRolesProvidedException.java new file mode 100644 index 0000000000..b847739027 --- /dev/null +++ b/quarkus-modules/quarkus-rbac/src/main/java/com/baeldung/quarkus/rbac/users/errors/InvalidRolesProvidedException.java @@ -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); + } +} diff --git a/quarkus-modules/quarkus-rbac/src/main/resources/application.properties b/quarkus-modules/quarkus-rbac/src/main/resources/application.properties new file mode 100644 index 0000000000..845c45f48e --- /dev/null +++ b/quarkus-modules/quarkus-rbac/src/main/resources/application.properties @@ -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 diff --git a/quarkus-modules/quarkus-rbac/src/main/resources/import.sql b/quarkus-modules/quarkus-rbac/src/main/resources/import.sql new file mode 100644 index 0000000000..cb0e3adfcd --- /dev/null +++ b/quarkus-modules/quarkus-rbac/src/main/resources/import.sql @@ -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'); diff --git a/quarkus-modules/quarkus-rbac/src/main/resources/privateKey.pem b/quarkus-modules/quarkus-rbac/src/main/resources/privateKey.pem new file mode 100644 index 0000000000..82e5e9bb6c --- /dev/null +++ b/quarkus-modules/quarkus-rbac/src/main/resources/privateKey.pem @@ -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----- \ No newline at end of file diff --git a/quarkus-modules/quarkus-rbac/src/main/resources/publicKey.pem b/quarkus-modules/quarkus-rbac/src/main/resources/publicKey.pem new file mode 100644 index 0000000000..12a18c1d18 --- /dev/null +++ b/quarkus-modules/quarkus-rbac/src/main/resources/publicKey.pem @@ -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----- \ No newline at end of file diff --git a/quarkus-modules/quarkus-rbac/src/test/java/com/baeldung/quarkus/PermissionBasedControllerIntegrationTest.java b/quarkus-modules/quarkus-rbac/src/test/java/com/baeldung/quarkus/PermissionBasedControllerIntegrationTest.java new file mode 100644 index 0000000000..55322b7613 --- /dev/null +++ b/quarkus-modules/quarkus-rbac/src/test/java/com/baeldung/quarkus/PermissionBasedControllerIntegrationTest.java @@ -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!")); + } + +} \ No newline at end of file diff --git a/quarkus-modules/quarkus-rbac/src/test/java/com/baeldung/quarkus/SecureResourceControllerIntegrationTest.java b/quarkus-modules/quarkus-rbac/src/test/java/com/baeldung/quarkus/SecureResourceControllerIntegrationTest.java new file mode 100644 index 0000000000..e23be3d37b --- /dev/null +++ b/quarkus-modules/quarkus-rbac/src/test/java/com/baeldung/quarkus/SecureResourceControllerIntegrationTest.java @@ -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!")); + } +} \ No newline at end of file