Skip to content

Commit

Permalink
feat: Added JWT RBAC to APIs (#5)
Browse files Browse the repository at this point in the history
Co-authored-by: fabrizio.guerrini <fabrizio.guerrini@C11-C9GE23PFRDO.dir.svc.accenture.com>
  • Loading branch information
dsabacn and fabrizio.guerrini authored Jun 23, 2023
1 parent 710b706 commit e87acd1
Show file tree
Hide file tree
Showing 51 changed files with 10,408 additions and 1,389 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,5 @@ nb-configuration.xml

src/main/terraform/identity/.terraform
src/main/terraform/identity/.terraform.lock.hcl

*.bak
622 changes: 353 additions & 269 deletions dep-sha256.json

Large diffs are not rendered by default.

15 changes: 14 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<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.1.0.Final</quarkus.platform.version>
<quarkus.platform.version>3.1.1.Final</quarkus.platform.version>
<skipITs>true</skipITs>
<depcheck-plugin.version>1.1.1</depcheck-plugin.version>
<common.version>2.0.2</common.version>
Expand Down Expand Up @@ -141,6 +141,19 @@
<artifactId>nimbus-jose-jwt</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-jwt</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-security-jwt</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-jwt-build</artifactId>
</dependency>
</dependencies>
<repositories>
<repository>
Expand Down
11 changes: 7 additions & 4 deletions src/main/java/it/pagopa/swclient/mil/preset/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,16 @@ public final class ErrorCode {

// business logic errors
public static final String SUBSCRIBER_NOT_FOUND = MODULE_ID + "00000F";
public static final String SUBSCRIBER_ALREADY_EXISTS = MODULE_ID + "00000F";
public static final String PRESET_OPERATION_NOT_FOUND = MODULE_ID + "000010";
public static final String SUBSCRIBER_ALREADY_EXISTS = MODULE_ID + "000010";
public static final String PRESET_OPERATION_NOT_FOUND = MODULE_ID + "000011";


// integration errors
public static final String ERROR_WRITING_DATA_IN_DB = MODULE_ID + "000011";
public static final String ERROR_READING_DATA_FROM_DB = MODULE_ID + "000012";
public static final String ERROR_WRITING_DATA_IN_DB = MODULE_ID + "000012";
public static final String ERROR_READING_DATA_FROM_DB = MODULE_ID + "000013";

public static final String ERROR_UNAUTHORIZED = MODULE_ID + "000014";
public static final String ERROR_FORBIDDEN = MODULE_ID + "000015";


private ErrorCode() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
public class CreatePresetRequest {

/*
* Operation of payment of a notice
* Type of preset operation
*/
@NotNull(message = "[" + ErrorCode.OPERATION_TYPE_MUST_NOT_BE_NULL + "] operationType must not be null")
@Pattern(regexp = "PAYMENT_NOTICE", message = "[" + ErrorCode.OPERATION_TYPE_MUST_MATCH_REGEXP + "] operationType must match \"{regexp}\"")
Expand Down
18 changes: 18 additions & 0 deletions src/main/java/it/pagopa/swclient/mil/preset/bean/Role.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package it.pagopa.swclient.mil.preset.bean;

public enum Role {

NODO("Nodo"),
NOTICE_PAYER("NoticePayer"),
INSTITUTION_PORTAL("InstitutionPortal"),
SERVICE_LIST_REQUESTER("ServiceListRequester"),
SLAVE_POS("SlavePos");

public final String label;

Role(String label) {
this.label = label;
}

}

Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public class SubscribeRequest {
* Mnemonic terminal label
*/
@NotNull(message = "[" + ErrorCode.LABEL_MUST_NOT_BE_NULL + "] label must not be null")
@Pattern(regexp = "^[\\u0001-\\uD7FF\\uE000-\\uFFFD\\u1000-\\u10FF]{1,256}$", message = "[" + ErrorCode.LABEL_MUST_MATCH_REGEXP + "] paTaxCode must match \"{regexp}\"")
@Pattern(regexp = "^[\\u0001-\\uD7FF\\uE000-\\uFFFD\\u1000-\\u10FF]{1,256}$", message = "[" + ErrorCode.LABEL_MUST_MATCH_REGEXP + "] label must match \"{regexp}\"")
private String label;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@

import it.pagopa.swclient.mil.ErrorCode;
import it.pagopa.swclient.mil.bean.Channel;
import it.pagopa.swclient.mil.preset.validation.constraints.MerchantIdNotNullForPos;
import it.pagopa.swclient.mil.preset.validation.constraints.AcquirerIdNotNullForRole;
import it.pagopa.swclient.mil.preset.validation.constraints.ChannelNotNullForRole;
import it.pagopa.swclient.mil.preset.validation.constraints.MerchantIdNotNullForRole;
import it.pagopa.swclient.mil.preset.validation.constraints.TerminalIdNotNullForRole;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import jakarta.ws.rs.HeaderParam;

@MerchantIdNotNullForPos(message = ErrorCode.MERCHANT_ID_MUST_NOT_BE_NULL_FOR_POS_MSG)
@MerchantIdNotNullForRole(roles = {Role.SLAVE_POS}, message = ErrorCode.MERCHANT_ID_MUST_NOT_BE_NULL_FOR_POS_MSG)
@AcquirerIdNotNullForRole(roles = {Role.SLAVE_POS}, message = ErrorCode.ACQUIRER_ID_MUST_NOT_BE_NULL_MSG)
@ChannelNotNullForRole(roles = {Role.SLAVE_POS}, message = ErrorCode.CHANNEL_MUST_NOT_BE_NULL_MSG)
@TerminalIdNotNullForRole(roles = {Role.SLAVE_POS}, message = ErrorCode.TERMINAL_ID_MUST_NOT_BE_NULL_MSG)
public class UnsubscribeHeaders {
/*
* Request ID
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import it.pagopa.swclient.mil.preset.dao.SubscriberEntity;
import it.pagopa.swclient.mil.preset.dao.SubscriberRepository;
import it.pagopa.swclient.mil.preset.utils.DateUtils;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
Expand Down Expand Up @@ -70,6 +71,7 @@ public class PresetsResource {
* @return an {@link Uni} emitting an empty 201 Created response with the Location header populated
*/
@POST
@RolesAllowed({ "InstitutionPortal" })
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Uni<Response> createPreset(@Valid @BeanParam InstitutionPortalHeaders portalHeaders,
Expand Down Expand Up @@ -112,6 +114,7 @@ public Uni<Response> createPreset(@Valid @BeanParam InstitutionPortalHeaders por
* @return an {@link Uni} emitting the list of {@link PresetOperation} configured for the subscriber
*/
@GET
@RolesAllowed({ "InstitutionPortal" })
@Produces(MediaType.APPLICATION_JSON)
@Path("/{paTaxCode}/{subscriberId}")
public Uni<Response> getPresets(@Valid @BeanParam InstitutionPortalHeaders headers, @Valid @BeanParam SubscriberPathParams pathParams) {
Expand All @@ -135,6 +138,7 @@ public Uni<Response> getPresets(@Valid @BeanParam InstitutionPortalHeaders heade
* @return an {@link Uni} emitting the latest {@link PresetOperation} configured for the subscriber
*/
@GET
@RolesAllowed({ "SlavePos" })
@Produces(MediaType.APPLICATION_JSON)
@Path("/{paTaxCode}/{subscriberId}/last_to_execute")
public Uni<Response> getLastPresetsOperation(@Valid @BeanParam CommonHeader headers, @Valid @BeanParam SubscriberPathParams pathParams) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import it.pagopa.swclient.mil.preset.dao.SubscriberEntity;
import it.pagopa.swclient.mil.preset.dao.SubscriberRepository;
import it.pagopa.swclient.mil.preset.utils.DateUtils;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
Expand All @@ -47,7 +48,7 @@ public class TerminalsResource {

@Inject
SubscriberRepository subscriberRepository;

/**
* The base URL for the location header returned by the subscribe API (i.e. the API management base URL)
*/
Expand All @@ -62,6 +63,7 @@ public class TerminalsResource {
*/
@GET
@Path("/{paTaxCode}")
@RolesAllowed({ "InstitutionPortal"})
@Produces(MediaType.APPLICATION_JSON)
public Uni<Response> getSubscribers(@Valid @BeanParam InstitutionPortalHeaders portalHeaders,

Expand Down Expand Up @@ -101,12 +103,14 @@ public Uni<Response> getSubscribers(@Valid @BeanParam InstitutionPortalHeaders p
* @return no content, if the terminal was successfully unsubscribed, or 404 if no subscriber was found with the given id
*/
@DELETE
@RolesAllowed({ "SlavePos","InstitutionPortal" })
@Produces(MediaType.APPLICATION_JSON)
@Path(value = "/{paTaxCode}/{subscriberId}")
public Uni<Response> unsubscribe(@Valid @BeanParam UnsubscribeHeaders headers, SubscriberPathParams pathParams) {
public Uni<Response> unsubscribe(@Valid @BeanParam UnsubscribeHeaders headers,
@Valid SubscriberPathParams pathParams) {

Log.debugf("unsubscribe - Input parameters: %s, %s", headers, pathParams);

return subscriberRepository.delete("subscriber.paTaxCode = :paTaxCode and subscriber.subscriberId = :subscriberId",
Parameters
.with("paTaxCode", pathParams.getPaTaxCode())
Expand Down Expand Up @@ -142,6 +146,7 @@ public Uni<Response> unsubscribe(@Valid @BeanParam UnsubscribeHeaders headers, S
* @return 201 if the terminal was subscribed
*/
@POST
@RolesAllowed({ "SlavePos" })
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Uni<Response> subscribe(@Valid @BeanParam CommonHeader commonHeader,
Expand Down Expand Up @@ -235,7 +240,9 @@ private SubscriberEntity buildSubscriberEntity(SubscribeRequest subscribeRequest
subscriber.setMerchantId(commonHeader.getMerchantId());
subscriber.setPaTaxCode(subscribeRequest.getPaTaxCode());
subscriber.setSubscriberId(subscriberId);
subscriber.setSubscriptionTimestamp(DateUtils.getCurrentTimestamp());
String currentTimestamp = DateUtils.getCurrentTimestamp();
subscriber.setSubscriptionTimestamp(currentTimestamp);
subscriber.setLastUsageTimestamp(currentTimestamp);
subscriber.setTerminalId(commonHeader.getTerminalId());

SubscriberEntity subscriberEntity = new SubscriberEntity();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package it.pagopa.swclient.mil.preset.utils;

import io.quarkus.logging.Log;
import io.quarkus.security.AuthenticationFailedException;
import it.pagopa.swclient.mil.preset.ErrorCode;
import jakarta.annotation.Priority;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;

@Provider
@Priority(Priorities.AUTHENTICATION)
public class AuthenticationFailedExceptionMapper implements ExceptionMapper<AuthenticationFailedException> {

@Context
ContainerRequestContext context;

@Override
public Response toResponse(AuthenticationFailedException exception) {
Log.errorf("[%s] Error unauthorized - %s", ErrorCode.ERROR_UNAUTHORIZED, context.getHeaders());
return Response.status(Response.Status.UNAUTHORIZED).build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package it.pagopa.swclient.mil.preset.utils;

import io.quarkus.logging.Log;
import io.quarkus.security.ForbiddenException;
import it.pagopa.swclient.mil.preset.ErrorCode;
import jakarta.annotation.Priority;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;

@Provider
@Priority(Priorities.AUTHENTICATION)
public class ForbiddenExceptionMapper implements ExceptionMapper<ForbiddenException> {

@Context
ContainerRequestContext context;

@Override
public Response toResponse(ForbiddenException exception) {
Log.errorf("[%s] Error forbidden - %s", ErrorCode.ERROR_FORBIDDEN, context.getHeaders());
return Response.status(Response.Status.FORBIDDEN).build();
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package it.pagopa.swclient.mil.preset.validation.constraints;

import it.pagopa.swclient.mil.preset.bean.Role;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Documented
@Retention(RUNTIME)
@Target(TYPE)
@Constraint(validatedBy = {
AcquirerIdNotNullForRoleValidator.class
})
public @interface AcquirerIdNotNullForRole {

String message() default "";

Role[] roles() default {};

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package it.pagopa.swclient.mil.preset.validation.constraints;

import it.pagopa.swclient.mil.preset.bean.UnsubscribeHeaders;
import jakarta.inject.Inject;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import org.eclipse.microprofile.jwt.JsonWebToken;

import java.util.Arrays;
import java.util.List;

public class AcquirerIdNotNullForRoleValidator implements ConstraintValidator<AcquirerIdNotNullForRole, UnsubscribeHeaders> {

@Inject
JsonWebToken jwt;

List<String> roles;

@Override
public void initialize(AcquirerIdNotNullForRole constraintAnnotation) {
roles = Arrays.stream(constraintAnnotation.roles()).map(r -> r.label).toList();
}

@Override
public boolean isValid(UnsubscribeHeaders unsubscribeHeaders, ConstraintValidatorContext context) {
String acquirerId = unsubscribeHeaders.getAcquirerId();
return (jwt.getGroups().stream().noneMatch(roles::contains) || acquirerId != null);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
/**
*
*/
package it.pagopa.swclient.mil.preset.validation.constraints;

import it.pagopa.swclient.mil.preset.bean.Role;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;

Expand All @@ -17,11 +15,14 @@
@Retention(RUNTIME)
@Target(TYPE)
@Constraint(validatedBy = {
MerchantIdNotNullForPosValidator.class
ChannelNotNullForRoleValidator.class
})
public @interface MerchantIdNotNullForPos {
public @interface ChannelNotNullForRole {

String message() default "";

Role[] roles() default {};

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
*
*/
package it.pagopa.swclient.mil.preset.validation.constraints;

import org.eclipse.microprofile.jwt.JsonWebToken;

import it.pagopa.swclient.mil.preset.bean.UnsubscribeHeaders;
import jakarta.inject.Inject;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

import java.util.Arrays;
import java.util.List;

public class ChannelNotNullForRoleValidator implements ConstraintValidator<ChannelNotNullForRole, UnsubscribeHeaders> {

@Inject
JsonWebToken jwt;

List<String> roles;

@Override
public void initialize(ChannelNotNullForRole constraintAnnotation) {
roles = Arrays.stream(constraintAnnotation.roles()).map(r -> r.label).toList();
}

@Override
public boolean isValid(UnsubscribeHeaders unsubscribeHeaders, ConstraintValidatorContext context) {
String channel = unsubscribeHeaders.getChannel();
return (jwt.getGroups().stream().noneMatch(roles::contains) || channel != null);

}
}
Loading

0 comments on commit e87acd1

Please sign in to comment.