From 7a31863f1bcb73d94269de4aef5e0002fa00dc05 Mon Sep 17 00:00:00 2001 From: Paulo Gomes da Cruz Junior Date: Mon, 9 Dec 2024 10:57:41 -0800 Subject: [PATCH 01/22] feat(FSADT1-1631): removing unused code --- .../controller/client/ClientController.java | 29 ++---------- .../service/bcregistry/BcRegistryService.java | 2 +- .../service/client/ClientLegacyService.java | 14 +----- .../gov/app/service/client/ClientService.java | 45 +++++++++---------- .../ClientLegacyServiceIntegrationTest.java | 2 +- .../client/ClientServiceIntegrationTest.java | 4 +- 6 files changed, 30 insertions(+), 66 deletions(-) diff --git a/backend/src/main/java/ca/bc/gov/app/controller/client/ClientController.java b/backend/src/main/java/ca/bc/gov/app/controller/client/ClientController.java index c1d48d06fd..487f54246a 100644 --- a/backend/src/main/java/ca/bc/gov/app/controller/client/ClientController.java +++ b/backend/src/main/java/ca/bc/gov/app/controller/client/ClientController.java @@ -64,32 +64,11 @@ public Mono getClientDetailsByIncorporationNumber( JwtPrincipalUtil.getProvider(principal) ); } - - /** - * Handles HTTP GET requests to retrieve client details based on the provided client number. - * - *

This method fetches the details of a client from the {@code ClientService} using the - * specified {@code clientNumber}. The caller's JWT authentication token is used to extract - * user-related information such as groups and user ID.

- * - * @param clientNumber the unique identifier of the client whose details are to be retrieved. - * @param principal the {@link JwtAuthenticationToken} containing the authenticated user's - * information, including their roles and groups. - * @return a {@link Mono} emitting the {@link ForestClientDetailsDto} containing the requested - * client details, or an error if the client cannot be found or accessed. - */ + @GetMapping("/details/{clientNumber}") - public Mono getClientDetailsByClientNumber( - @PathVariable String clientNumber, - JwtAuthenticationToken principal - ) { - log.info("Requesting client details for client number {} from the client service. {}", - clientNumber, - JwtPrincipalUtil.getUserId(principal) - ); - return clientService.getClientDetailsByClientNumber( - clientNumber, - JwtPrincipalUtil.getGroups(principal)); + public Mono getClientDetailsByClientNumber(@PathVariable String clientNumber) { + log.info("Requesting client details for client number {}", clientNumber); + return clientService.getClientDetailsByClientNumber(clientNumber); } /** diff --git a/backend/src/main/java/ca/bc/gov/app/service/bcregistry/BcRegistryService.java b/backend/src/main/java/ca/bc/gov/app/service/bcregistry/BcRegistryService.java index 88fe6b5be3..2e8fb32c52 100644 --- a/backend/src/main/java/ca/bc/gov/app/service/bcregistry/BcRegistryService.java +++ b/backend/src/main/java/ca/bc/gov/app/service/bcregistry/BcRegistryService.java @@ -72,7 +72,7 @@ public BcRegistryService( * @throws InvalidAccessTokenException if the access token is invalid or expired */ public Flux searchByFacets(String name, String identifier) { - log.info("Searching BC Registry for {}", name); + log.info("Searching BC Registry for {}", Objects.toString(name,identifier)); return bcRegistryApi .post() diff --git a/backend/src/main/java/ca/bc/gov/app/service/client/ClientLegacyService.java b/backend/src/main/java/ca/bc/gov/app/service/client/ClientLegacyService.java index 98f28e4acb..872deba389 100644 --- a/backend/src/main/java/ca/bc/gov/app/service/client/ClientLegacyService.java +++ b/backend/src/main/java/ca/bc/gov/app/service/client/ClientLegacyService.java @@ -97,25 +97,15 @@ public Flux searchLegacy( * criteria. If a matching record is found, it is returned as a {@link ForestClientDetailsDto}. * * @param clientNumber the client number to search for - * @param groups a list of groups to filter the search (optional) * @return a {@link Mono} emitting the {@link ForestClientDetailsDto} if the client is found */ - public Mono searchByClientNumber( - String clientNumber, - List groups - ) { + public Mono searchByClientNumber(String clientNumber) { log.info("Searching for client number {} in legacy", clientNumber); return legacyApi .get() - .uri(builder -> - builder - .path("/api/search/clientNumber") - .queryParam("clientNumber", clientNumber) - .queryParam("groups", groups) - .build(Map.of()) - ) + .uri("/api/search/clientNumber/{clientNumber}", clientNumber) .exchangeToMono(response -> response.bodyToMono(ForestClientDetailsDto.class)) .doOnNext( dto -> log.info( diff --git a/backend/src/main/java/ca/bc/gov/app/service/client/ClientService.java b/backend/src/main/java/ca/bc/gov/app/service/client/ClientService.java index 4ee8e1203f..56b8f8332a 100644 --- a/backend/src/main/java/ca/bc/gov/app/service/client/ClientService.java +++ b/backend/src/main/java/ca/bc/gov/app/service/client/ClientService.java @@ -144,32 +144,27 @@ public Mono getClientDetailsByIncorporationNumber( ))); } - public Mono getClientDetailsByClientNumber( - String clientNumber, - List groups - ) { - log.info("Loading details for {} for user role {}", clientNumber, groups.toString()); - + public Mono getClientDetailsByClientNumber(String clientNumber) { return legacyService - .searchByClientNumber(clientNumber, groups) - .flatMap(forestClientDetailsDto -> { - String corpRegnNmbr = forestClientDetailsDto.corpRegnNmbr(); - - if (corpRegnNmbr == null || corpRegnNmbr.isEmpty()) { - log.info("Corporation registration number not provided. Returning legacy details."); - return Mono.just(forestClientDetailsDto); - } - - log.info("Retrieved corporation registration number: {}", corpRegnNmbr); - - return bcRegistryService - .requestDocumentData(corpRegnNmbr) - .next() - .flatMap(documentMono -> - populateGoodStandingInd(forestClientDetailsDto, - documentMono) - ); - }); + .searchByClientNumber(clientNumber) + .flatMap(forestClientDetailsDto -> Mono + .just(forestClientDetailsDto) + .filter(dto ->(StringUtils.isNotBlank(dto.corpRegnNmbr()))) + .doOnNext(dto -> log.info("Retrieved corporation registration number: {}", forestClientDetailsDto.corpRegnNmbr())) + .flatMap(dto -> + bcRegistryService + .requestDocumentData( dto.corpRegnNmbr()) + .next() + ) + .flatMap(documentMono -> populateGoodStandingInd(forestClientDetailsDto, documentMono) ) + .onErrorContinue(NoClientDataFound.class, (ex, obj) -> + log.error("No data found on BC Registry for client number: {}", clientNumber) + ) + .switchIfEmpty( + Mono.just(forestClientDetailsDto) + .doOnNext(dto -> log.info("Corporation registration number not provided. Returning legacy details.")) + ) + ); } private Mono populateGoodStandingInd( diff --git a/backend/src/test/java/ca/bc/gov/app/service/client/ClientLegacyServiceIntegrationTest.java b/backend/src/test/java/ca/bc/gov/app/service/client/ClientLegacyServiceIntegrationTest.java index 09d123c2a8..662561af39 100644 --- a/backend/src/test/java/ca/bc/gov/app/service/client/ClientLegacyServiceIntegrationTest.java +++ b/backend/src/test/java/ca/bc/gov/app/service/client/ClientLegacyServiceIntegrationTest.java @@ -214,7 +214,7 @@ void shouldSearchLegacyByClientNumber() { .withHeader("Content-Type", equalTo("application/json")) ); - service.searchByClientNumber(clientNumber, groups) + service.searchByClientNumber(clientNumber) .as(StepVerifier::create) .assertNext(clientDetailsDto -> { assertThat(clientDetailsDto) diff --git a/backend/src/test/java/ca/bc/gov/app/service/client/ClientServiceIntegrationTest.java b/backend/src/test/java/ca/bc/gov/app/service/client/ClientServiceIntegrationTest.java index 779d4f1b75..b937e5a945 100644 --- a/backend/src/test/java/ca/bc/gov/app/service/client/ClientServiceIntegrationTest.java +++ b/backend/src/test/java/ca/bc/gov/app/service/client/ClientServiceIntegrationTest.java @@ -153,7 +153,7 @@ void testGetClientDetailsWithGoodStandingIndicator() { null); Mockito - .when(legacyService.searchByClientNumber(clientNumber, groups)) + .when(legacyService.searchByClientNumber(clientNumber)) .thenReturn(Mono.just(initialDto)); Mockito @@ -162,7 +162,7 @@ void testGetClientDetailsWithGoodStandingIndicator() { .thenReturn(Flux.just(mockDocumentDto)); service - .getClientDetailsByClientNumber(clientNumber, groups) + .getClientDetailsByClientNumber(clientNumber) .as(StepVerifier::create) .expectNext(expectedDto) .verifyComplete(); From d277516a2c90b9d586dc69ebb26af3995f6623fc Mon Sep 17 00:00:00 2001 From: Paulo Gomes da Cruz Junior Date: Mon, 9 Dec 2024 11:29:49 -0800 Subject: [PATCH 02/22] chore: updating legacy api --- .../gov/app/controller/ClientSearchController.java | 14 ++++---------- .../ca/bc/gov/app/service/ClientSearchService.java | 8 ++------ 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/legacy/src/main/java/ca/bc/gov/app/controller/ClientSearchController.java b/legacy/src/main/java/ca/bc/gov/app/controller/ClientSearchController.java index 4c64a0cf58..b4f058c01c 100644 --- a/legacy/src/main/java/ca/bc/gov/app/controller/ClientSearchController.java +++ b/legacy/src/main/java/ca/bc/gov/app/controller/ClientSearchController.java @@ -80,18 +80,12 @@ public Flux findByIdAndLastName( * Handles the HTTP GET request to retrieve client details by client number. * * @param clientNumber the client number to search for - * @param groups the list of user groups for authorization or filtering purposes * @return a Mono containing the client details if found, or an empty Mono if not found */ - @GetMapping("/clientNumber") - public Mono findByClientNumber( - @RequestParam String clientNumber, - @RequestParam List groups - ) { - log.info("Receiving request to search by ID {} and groups {}", - clientNumber, - groups); - return service.findByClientNumber(clientNumber, groups); + @GetMapping("/clientNumber/{clientNumber}") + public Mono findByClientNumber(@PathVariable String clientNumber) { + log.info("Receiving request to search by ID {}",clientNumber); + return service.findByClientNumber(clientNumber); } @GetMapping("/id/{idType}/{identification}") diff --git a/legacy/src/main/java/ca/bc/gov/app/service/ClientSearchService.java b/legacy/src/main/java/ca/bc/gov/app/service/ClientSearchService.java index f7546bde53..4c6b54c296 100644 --- a/legacy/src/main/java/ca/bc/gov/app/service/ClientSearchService.java +++ b/legacy/src/main/java/ca/bc/gov/app/service/ClientSearchService.java @@ -541,21 +541,17 @@ public Flux findByClientName(String clientName) { ); } - public Mono findByClientNumber(String clientNumber, List groups) { + public Mono findByClientNumber(String clientNumber) { log.info("Searching for client with number {}", clientNumber); if (StringUtils.isBlank(clientNumber)) { return Mono.error(new MissingRequiredParameterException("clientNumber")); } - - if (CollectionUtils.isEmpty(groups)) { - return Mono.error(new MissingRequiredParameterException("groups")); - } return forestClientRepository.findDetailsByClientNumber(clientNumber) .switchIfEmpty( Mono.error( - new NoValueFoundException("Client not found with number: " + clientNumber) + new NoValueFoundException("client with number: " + clientNumber) ) ) .doOnNext( From 0bc8600966ccdfd5d4da2b07c98979a551727e64 Mon Sep 17 00:00:00 2001 From: Paulo Gomes da Cruz Junior Date: Tue, 17 Dec 2024 07:06:35 -0800 Subject: [PATCH 03/22] feat(FSADT1-1631): added context propagator for roles - Reused the MDC for role context propagator. - Refactored the UserID filter - Extracted the context propagator filter --- .../filters/ContextPropagatorWebFilter.java | 75 +++++++++++++++ .../controller/filters/UserIdWebFilter.java | 94 ++++--------------- .../filters/UserRolesWebFilter.java | 55 +++++++++++ 3 files changed, 150 insertions(+), 74 deletions(-) create mode 100644 backend/src/main/java/ca/bc/gov/app/controller/filters/ContextPropagatorWebFilter.java create mode 100644 backend/src/main/java/ca/bc/gov/app/controller/filters/UserRolesWebFilter.java diff --git a/backend/src/main/java/ca/bc/gov/app/controller/filters/ContextPropagatorWebFilter.java b/backend/src/main/java/ca/bc/gov/app/controller/filters/ContextPropagatorWebFilter.java new file mode 100644 index 0000000000..0ad9a92882 --- /dev/null +++ b/backend/src/main/java/ca/bc/gov/app/controller/filters/ContextPropagatorWebFilter.java @@ -0,0 +1,75 @@ +package ca.bc.gov.app.controller.filters; + +import io.micrometer.context.ContextRegistry; +import java.util.function.Function; +import org.slf4j.MDC; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; +import reactor.util.context.Context; + +public abstract class ContextPropagatorWebFilter implements WebFilter { + + protected abstract String getContextKey(); + + protected abstract Function getContextValueExtractor(); + + /** + * Filters each incoming request to extract from the security context a {@link String} value using + * {@link #getContextValueExtractor()} and set it in the MDC. + * The value is then propagated down the filter chain and set in the reactive context + * to ensure it is available for logging and tracing in both reactive and non-reactive parts of + * the application. + * + * @param exchange The current server web exchange that contains information about the request and + * response. + * @param chain The web filter chain that allows the filter to pass on the request to the next + * entity in the chain. + * @return A Mono that indicates when request handling is complete. + */ + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + // This is to be able to tackle non-reactive context + contextLoad(); + + return + // Here we are getting the user id from the security context + ReactiveSecurityContextHolder + .getContext() + .map(SecurityContext::getAuthentication) + .map(getContextValueExtractor()) + // Then we set it to the MDC + .doOnNext(userId -> MDC.put(getContextKey(), userId)) + // Then we chain the filter, passing the context down + .flatMap(userId -> chain + .filter(exchange) + .contextWrite(Context.of(getContextKey(), userId)) + // While we are at it, we also set the context for the reactive part + .doOnNext(v -> contextLoad()) + ); + } + + /** + * Initializes and registers a thread-local context for the current thread. This method configures + * the {@link ContextRegistry} to handle the value within the MDC (Mapped Diagnostic Context), + * allowing for the propagation of the value across different parts of the application that run + * on the same thread. It specifically sets up accessors for getting, setting, and removing the + * value from the MDC. This setup is crucial for maintaining state across the reactive and + * non-reactive parts of the application. + */ + private void contextLoad() { + ContextRegistry + .getInstance() + .registerThreadLocalAccessor( + getContextKey(), + () -> MDC.get(getContextKey()), + userId -> MDC.put(getContextKey(), userId), + () -> MDC.remove(getContextKey()) + ); + } + +} diff --git a/backend/src/main/java/ca/bc/gov/app/controller/filters/UserIdWebFilter.java b/backend/src/main/java/ca/bc/gov/app/controller/filters/UserIdWebFilter.java index 461c18264a..45875b43db 100644 --- a/backend/src/main/java/ca/bc/gov/app/controller/filters/UserIdWebFilter.java +++ b/backend/src/main/java/ca/bc/gov/app/controller/filters/UserIdWebFilter.java @@ -2,22 +2,14 @@ import ca.bc.gov.app.ApplicationConstant; import ca.bc.gov.app.util.JwtPrincipalUtil; -import io.micrometer.context.ContextRegistry; +import java.util.function.Function; import lombok.extern.slf4j.Slf4j; -import org.slf4j.MDC; import org.springframework.core.annotation.Order; import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.ReactiveSecurityContextHolder; -import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.springframework.stereotype.Component; -import org.springframework.web.server.ServerWebExchange; -import org.springframework.web.server.WebFilter; -import org.springframework.web.server.WebFilterChain; -import reactor.core.publisher.Mono; -import reactor.util.context.Context; /** @@ -30,59 +22,11 @@ @Component @Slf4j @Order(-1) -public class UserIdWebFilter implements WebFilter { +public class UserIdWebFilter extends ContextPropagatorWebFilter { - /** - * Filters each incoming request to extract the user ID from the security context and set it in - * the MDC. The user ID is then propagated down the filter chain and set in the reactive context - * to ensure it is available for logging and tracing in both reactive and non-reactive parts of - * the application. - * - * @param exchange The current server web exchange that contains information about the request and - * response. - * @param chain The web filter chain that allows the filter to pass on the request to the next - * entity in the chain. - * @return A Mono that indicates when request handling is complete. - */ @Override - public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { - // This is to be able to tackle non-reactive context - contextLoad(); - - return - // Here we are getting the user id from the security context - ReactiveSecurityContextHolder - .getContext() - .map(SecurityContext::getAuthentication) - .map(this::extractUserId) - // Then we set it to the MDC - .doOnNext(userId -> MDC.put(ApplicationConstant.MDC_USERID, userId)) - // Then we chain the filter, passing the context down - .flatMap(userId -> chain - .filter(exchange) - .contextWrite(Context.of(ApplicationConstant.MDC_USERID, userId)) - // While we are at it, we also set the context for the reactive part - .doOnNext(v -> contextLoad()) - ); - } - - /** - * Initializes and registers a thread-local context for the current thread. This method configures - * the {@link ContextRegistry} to handle the user ID within the MDC (Mapped Diagnostic Context), - * allowing for the propagation of the user ID across different parts of the application that run - * on the same thread. It specifically sets up accessors for getting, setting, and removing the - * user ID from the MDC. This setup is crucial for maintaining state across the reactive and - * non-reactive parts of the application. - */ - private void contextLoad() { - ContextRegistry - .getInstance() - .registerThreadLocalAccessor( - ApplicationConstant.MDC_USERID, - () -> MDC.get(ApplicationConstant.MDC_USERID), - userId -> MDC.put(ApplicationConstant.MDC_USERID, userId), - () -> MDC.remove(ApplicationConstant.MDC_USERID) - ); + protected String getContextKey() { + return ApplicationConstant.MDC_USERID; } /** @@ -93,27 +37,29 @@ private void contextLoad() { * If the authentication object is null, not authenticated, or the principal type is not * supported, "no-user-id-found" is returned. * - * @param authentication The authentication object from which to extract the user ID. * @return The extracted user ID in the format "Provider\\UserID" or the username, or * "no-user-id-found" if it cannot be extracted. */ - private String extractUserId(Authentication authentication) { - if (authentication != null && authentication.isAuthenticated()) { - Object principal = authentication.getPrincipal(); + @Override + protected Function getContextValueExtractor() { + return authentication -> { + if (authentication != null && authentication.isAuthenticated()) { + Object principal = authentication.getPrincipal(); - if (principal instanceof JwtAuthenticationToken jwt) { - return JwtPrincipalUtil.getUserId(jwt); - } + if (principal instanceof JwtAuthenticationToken jwt) { + return JwtPrincipalUtil.getUserId(jwt); + } - if (principal instanceof Jwt jwt) { - return JwtPrincipalUtil.getUserId(jwt); - } + if (principal instanceof Jwt jwt) { + return JwtPrincipalUtil.getUserId(jwt); + } - if (principal instanceof UserDetails userDetails) { - return userDetails.getUsername(); + if (principal instanceof UserDetails userDetails) { + return userDetails.getUsername(); + } } - } - return "no-user-id-found"; + return "no-user-id-found"; + }; } } diff --git a/backend/src/main/java/ca/bc/gov/app/controller/filters/UserRolesWebFilter.java b/backend/src/main/java/ca/bc/gov/app/controller/filters/UserRolesWebFilter.java new file mode 100644 index 0000000000..806948ccb2 --- /dev/null +++ b/backend/src/main/java/ca/bc/gov/app/controller/filters/UserRolesWebFilter.java @@ -0,0 +1,55 @@ +package ca.bc.gov.app.controller.filters; + +import ca.bc.gov.app.ApplicationConstant; +import ca.bc.gov.app.util.JwtPrincipalUtil; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.annotation.Order; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +@Order(-2) +public class UserRolesWebFilter extends ContextPropagatorWebFilter { + + @Override + protected String getContextKey() { + return ApplicationConstant.MDC_USERROLES; + } + + @Override + protected Function getContextValueExtractor() { + return authentication -> { + Set roles = Set.of(); + if (authentication != null && authentication.isAuthenticated()) { + Object principal = authentication.getPrincipal(); + + if (principal instanceof JwtAuthenticationToken jwt) { + roles = JwtPrincipalUtil.getGroups(jwt); + } + + if (principal instanceof Jwt jwt) { + roles = JwtPrincipalUtil.getGroups(jwt); + } + + if (principal instanceof UserDetails userDetails) { + roles = userDetails + .getAuthorities() + .stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + + } + } + return String.join(",", roles); + }; + } + +} From 80459894e7c7dd1c63ff8d2aaa826d9d8ad65298 Mon Sep 17 00:00:00 2001 From: Paulo Gomes da Cruz Junior Date: Tue, 17 Dec 2024 07:06:49 -0800 Subject: [PATCH 04/22] chore: small update on the DTO --- .../java/ca/bc/gov/app/dto/legacy/ForestClientDetailsDto.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/ca/bc/gov/app/dto/legacy/ForestClientDetailsDto.java b/backend/src/main/java/ca/bc/gov/app/dto/legacy/ForestClientDetailsDto.java index 2a83f8f709..7077f27654 100644 --- a/backend/src/main/java/ca/bc/gov/app/dto/legacy/ForestClientDetailsDto.java +++ b/backend/src/main/java/ca/bc/gov/app/dto/legacy/ForestClientDetailsDto.java @@ -26,10 +26,10 @@ public record ForestClientDetailsDto( String clientCommentUpdateUser, String goodStandingInd, LocalDate birthdate, - + List addresses, List contacts, List doingBusinessAs ) { - + } From b146cf24a13ec0c10ae585f8066ee2772d9dc3e5 Mon Sep 17 00:00:00 2001 From: Paulo Gomes da Cruz Junior Date: Tue, 17 Dec 2024 07:07:04 -0800 Subject: [PATCH 05/22] chore: added the role mdc constant --- backend/src/main/java/ca/bc/gov/app/ApplicationConstant.java | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/main/java/ca/bc/gov/app/ApplicationConstant.java b/backend/src/main/java/ca/bc/gov/app/ApplicationConstant.java index 4fcbbbe393..bac4f5843b 100644 --- a/backend/src/main/java/ca/bc/gov/app/ApplicationConstant.java +++ b/backend/src/main/java/ca/bc/gov/app/ApplicationConstant.java @@ -112,5 +112,6 @@ left join nrfc.province_code pc on (pc.province_code = sl.province_code and pc.c public static final String MDC_USERID = "X-USER"; + public static final String MDC_USERROLES = "X-Role"; } From bd233215db390cbffeba0a2544dbff8e66c1da81 Mon Sep 17 00:00:00 2001 From: Paulo Gomes da Cruz Junior Date: Tue, 17 Dec 2024 07:08:09 -0800 Subject: [PATCH 06/22] feat(FSADT1-1631): updated principal util roles to set - Changed to SET from LIST to account for unique values --- .../ca/bc/gov/app/util/JwtPrincipalUtil.java | 82 ++++++++++++------- .../gov/app/utils/JwtPrincipalUtilTest.java | 15 ++-- 2 files changed, 60 insertions(+), 37 deletions(-) diff --git a/backend/src/main/java/ca/bc/gov/app/util/JwtPrincipalUtil.java b/backend/src/main/java/ca/bc/gov/app/util/JwtPrincipalUtil.java index df2147df20..d3b1192948 100644 --- a/backend/src/main/java/ca/bc/gov/app/util/JwtPrincipalUtil.java +++ b/backend/src/main/java/ca/bc/gov/app/util/JwtPrincipalUtil.java @@ -5,6 +5,8 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; import java.util.stream.Stream; import lombok.NoArgsConstructor; import org.apache.commons.lang3.StringUtils; @@ -202,6 +204,55 @@ public static String getLastName(Jwt principal) { return getLastNameValue(principal.getClaims()); } + /** + * Retrieves a list of groups from the given JwtPrincipal. + * This method extracts the token attributes from the provided {@link JwtAuthenticationToken}, then looks for the key "cognito:groups" + * in the token attributes. If the value associated with this key is a {@link List}, the method filters the elements to only + * include non-null values of type {@link String}. The resulting list of strings is returned. + * + * @param jwtPrincipal The {@link JwtAuthenticationToken} containing the token attributes. It must have the "cognito:groups" key. + * If the key does not exist or the value is not a list of strings, an empty list is returned. + * @return A list of group names, or an empty list if the key is missing or the value is not a list of strings. + */ + public static Set getGroups(JwtAuthenticationToken jwtPrincipal) { + if (jwtPrincipal == null || jwtPrincipal.getTokenAttributes() == null) { + return Collections.emptySet(); + } + return getClaimGroups(jwtPrincipal.getTokenAttributes()); + } + + /** + * Retrieves a list of groups from the given JwtPrincipal. + * This method extracts the token attributes from the provided {@link Jwt}, then looks for the key "cognito:groups" + * in the token attributes. If the value associated with this key is a {@link List}, the method filters the elements to only + * include non-null values of type {@link String}. The resulting list of strings is returned. + * + * @param jwtPrincipal The {@link Jwt} containing the token attributes. It must have the "cognito:groups" key. + * If the key does not exist or the value is not a list of strings, an empty list is returned. + * @return A list of group names, or an empty list if the key is missing or the value is not a list of strings. + */ + public static Set getGroups(Jwt jwtPrincipal) { + if (jwtPrincipal == null || jwtPrincipal.getClaims() == null) { + return Collections.emptySet(); + } + return getClaimGroups(jwtPrincipal.getClaims()); + } + + private static Set getClaimGroups(Map tokenAttributes) { + Object groups = tokenAttributes.get("cognito:groups"); + System.out.println(groups); + + if (groups instanceof List) { + return ((List) groups).stream() + .filter(Objects::nonNull) + .filter(String.class::isInstance) + .map(String.class::cast) + .collect(Collectors.toSet()); + } + + return Collections.emptySet(); + } + /** * Retrieves the value of a specified claim from the claims map. If the claim is not present, * returns an empty string. @@ -383,35 +434,6 @@ private static String getLastNameValue(Map claims) { private static String getNameValue(Map claims) { return processName(claims).get("fullName"); } - - /** - * Retrieves a list of groups from the given JwtPrincipal. - * - * This method extracts the token attributes from the provided {@link JwtPrincipal}, then looks for the key "cognito:groups" - * in the token attributes. If the value associated with this key is a {@link List}, the method filters the elements to only - * include non-null values of type {@link String}. The resulting list of strings is returned. - * - * @param jwtPrincipal The {@link JwtPrincipal} containing the token attributes. It must have the "cognito:groups" key. - * If the key does not exist or the value is not a list of strings, an empty list is returned. - * @return A list of group names, or an empty list if the key is missing or the value is not a list of strings. - */ - public static List getGroups(JwtAuthenticationToken jwtPrincipal) { - if (jwtPrincipal == null || jwtPrincipal.getTokenAttributes() == null) { - return Collections.emptyList(); - } - - Map tokenAttributes = jwtPrincipal.getTokenAttributes(); - Object groups = tokenAttributes.get("cognito:groups"); - - if (groups instanceof List) { - return ((List) groups).stream() - .filter(Objects::nonNull) - .filter(String.class::isInstance) - .map(String.class::cast) - .toList(); - } - - return Collections.emptyList(); - } + } diff --git a/backend/src/test/java/ca/bc/gov/app/utils/JwtPrincipalUtilTest.java b/backend/src/test/java/ca/bc/gov/app/utils/JwtPrincipalUtilTest.java index 6da8e96b39..a5b2d6ebf9 100644 --- a/backend/src/test/java/ca/bc/gov/app/utils/JwtPrincipalUtilTest.java +++ b/backend/src/test/java/ca/bc/gov/app/utils/JwtPrincipalUtilTest.java @@ -8,6 +8,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Stream; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.DisplayName; @@ -186,12 +187,12 @@ private JwtAuthenticationToken createJwtAuthenticationTokenWithAttributes( @ParameterizedTest @DisplayName("getGroups should return expected group list") @MethodSource("provideGroupsTestData") - void shouldGetGroups(Map tokenAttributes, List expectedGroups) { + void shouldGetGroups(Map tokenAttributes, Set expectedGroups) { JwtAuthenticationToken jwtAuthenticationToken = tokenAttributes == null ? null : createJwtAuthenticationTokenWithAttributes(tokenAttributes); - List actualGroups = JwtPrincipalUtil.getGroups(jwtAuthenticationToken); + Set actualGroups = JwtPrincipalUtil.getGroups(jwtAuthenticationToken); assertEquals(expectedGroups, actualGroups); } @@ -201,29 +202,29 @@ private static Stream provideGroupsTestData() { // Case 1: Token attributes contain "CLIENT_ADMIN" Arguments.of( Map.of("cognito:groups", List.of("CLIENT_ADMIN")), - List.of("CLIENT_ADMIN") + Set.of("CLIENT_ADMIN") ), // Case 2: Token attributes contain an empty group list Arguments.of( Map.of("cognito:groups", List.of()), - List.of() + Set.of() ), // Case 3: Token attributes contain null groups Arguments.of( new HashMap<>() {{ put("cognito:groups", null); }}, - List.of() + Set.of() ), // Case 4: Token attributes missing "cognito:groups" Arguments.of( Map.of("otherKey", "someValue"), - List.of() + Set.of() ), // Case 5: Null JwtAuthenticationToken Arguments.of( null, - List.of() + Set.of() ) ); } From eb7ba7c1e2d22cb5c7f4bdea1efda0a818bd037d Mon Sep 17 00:00:00 2001 From: Paulo Gomes da Cruz Junior Date: Tue, 17 Dec 2024 07:09:01 -0800 Subject: [PATCH 07/22] feat(FSADT1-1631): added client details obfuscator - This will obfuscate values based on roles and fields --- .../GlobalServiceConfiguration.java | 18 ++- ...ForestClientDetailsSerializerModifier.java | 26 ++++ .../app/converters/ForestClientObfuscate.java | 127 ++++++++++++++++++ 3 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 backend/src/main/java/ca/bc/gov/app/converters/ForestClientDetailsSerializerModifier.java create mode 100644 backend/src/main/java/ca/bc/gov/app/converters/ForestClientObfuscate.java diff --git a/backend/src/main/java/ca/bc/gov/app/configuration/GlobalServiceConfiguration.java b/backend/src/main/java/ca/bc/gov/app/configuration/GlobalServiceConfiguration.java index c642473077..a00bebe9a7 100644 --- a/backend/src/main/java/ca/bc/gov/app/configuration/GlobalServiceConfiguration.java +++ b/backend/src/main/java/ca/bc/gov/app/configuration/GlobalServiceConfiguration.java @@ -1,5 +1,6 @@ package ca.bc.gov.app.configuration; +import ca.bc.gov.app.converters.ForestClientDetailsSerializerModifier; import ca.bc.gov.app.dto.ValidationError; import ca.bc.gov.app.dto.bcregistry.BcRegistryAddressDto; import ca.bc.gov.app.dto.bcregistry.BcRegistryAlternateNameDto; @@ -54,6 +55,8 @@ import ca.bc.gov.app.health.HealthExchangeFilterFunction; import ca.bc.gov.app.health.ManualHealthIndicator; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import lombok.extern.slf4j.Slf4j; import org.springframework.aot.hint.annotation.RegisterReflectionForBinding; import org.springframework.beans.factory.annotation.Qualifier; @@ -305,7 +308,20 @@ public WebClient processorApi( @Bean public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) { - return builder.build(); + + ObjectMapper mapper = builder.build(); + mapper.registerModule(new JavaTimeModule()); + mapper.registerModule(forestClientDetailsDtoModule()); + + return mapper; + } + + SimpleModule forestClientDetailsDtoModule() { + SimpleModule module = new SimpleModule(); + + // Register the serializer modifier + module.setSerializerModifier(new ForestClientDetailsSerializerModifier()); + return module; } } diff --git a/backend/src/main/java/ca/bc/gov/app/converters/ForestClientDetailsSerializerModifier.java b/backend/src/main/java/ca/bc/gov/app/converters/ForestClientDetailsSerializerModifier.java new file mode 100644 index 0000000000..57746fd530 --- /dev/null +++ b/backend/src/main/java/ca/bc/gov/app/converters/ForestClientDetailsSerializerModifier.java @@ -0,0 +1,26 @@ +package ca.bc.gov.app.converters; + +import ca.bc.gov.app.dto.legacy.ForestClientDetailsDto; +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializationConfig; +import com.fasterxml.jackson.databind.ser.BeanSerializerModifier; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class ForestClientDetailsSerializerModifier extends BeanSerializerModifier { + + @Override + public JsonSerializer modifySerializer( + SerializationConfig config, + BeanDescription beanDesc, + JsonSerializer serializer + ) { + + if (ForestClientDetailsDto.class.isAssignableFrom(beanDesc.getBeanClass())) { + return new ForestClientObfuscate<>(); + } + + return super.modifySerializer(config, beanDesc, serializer); + } +} diff --git a/backend/src/main/java/ca/bc/gov/app/converters/ForestClientObfuscate.java b/backend/src/main/java/ca/bc/gov/app/converters/ForestClientObfuscate.java new file mode 100644 index 0000000000..9b2c014232 --- /dev/null +++ b/backend/src/main/java/ca/bc/gov/app/converters/ForestClientObfuscate.java @@ -0,0 +1,127 @@ +package ca.bc.gov.app.converters; + +import ca.bc.gov.app.ApplicationConstant; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import java.io.IOException; +import java.time.LocalDate; +import java.util.List; +import java.util.Set; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.MDC; + +/** + * Requirement - ID - Suppress GIVEN Client detail summary edit mode WHEN in edit mode and role is + * CLIENT_VIEW THEN system will display the ID using the following format IF ( ID IS NOT BC Registry + * OR BC Service Card) THEN (example British Columbia Drivers License) { IF ( length < 7 values) + * THEN display the entire values ELSE [first value]***[Last three values] Example "1***756" END IF + * } ELSE IF ( BC Registry) Display the entire value. Example "FM232109" ELSE IF (BC Service Card) + * Display "BC Service card verified" END IF + *

+ * Requirement - Birthday GIVEN Client detail summary edit mode WHEN in edit mode and role is + * CLIENT_VIEW THEN system will only show year of birth year. Example "1983" AND Column name is + * "Year of birth" as oppose to "Date of birth" + */ +@Slf4j +public class ForestClientObfuscate extends JsonSerializer { + + private final List obfuscableFields = List.of("clientIdentification", "birthdate"); + + @SneakyThrows + @Override + public void serialize( + T value, + JsonGenerator gen, + SerializerProvider provider + ) throws IOException { + + if (value == null) { + gen.writeNull(); + } + + gen.writeStartObject(); + + var beanProps = provider.getConfig().introspect(provider.constructType(value.getClass())) + .findProperties(); + + String clientIdTypeCode = null; + + for (var property : beanProps) { + var propName = property.getName(); + var rawValue = property.getAccessor().getValue(value); + + // Use the default serializer for other fields + gen.writeFieldName(propName); + + // Skip null values entirely + if (rawValue == null) { + gen.writeNull(); + } else { + // Client identification type retrieval, hopefully this is the field comes before the value + if ("clientIdTypeCode".equals(propName)) { + clientIdTypeCode = rawValue.toString(); + } + + if (obfuscableFields.contains(propName)) { + gen.writeString(obfuscate(propName, clientIdTypeCode, rawValue)); + continue; + } + + var serializer = provider.findValueSerializer(property.getRawPrimaryType()); + serializer.serialize(rawValue, gen, provider); + } + } + + gen.writeEndObject(); + } + + private String obfuscate(String propName, String propType, Object value) { + Set roles = toRoles(MDC.get(ApplicationConstant.MDC_USERROLES)); + + if (!roles.contains(ApplicationConstant.ROLE_VIEWER)) { + return value.toString(); + } + + // BC Services card uses a UUID, so we just say it is verified + if ("clientIdentification".equals(propName) && "BCSC".equals(propType)) { + return "BC Service card verified"; + } + + if ("clientIdentification".equals(propName) && "BCRE".equals(propType)) { + return value.toString(); + } + + if ("clientIdentification".equals(propName)) { + return obfuscateClientIdentification(value.toString()); + } + + if ("birthdate".equals(propName)) { + return obfuscateBirthdate((LocalDate) value); + } + + return value.toString(); + } + + private String obfuscateBirthdate(LocalDate value) { + return String.format("%d-**-**", value.getYear()); + } + + private String obfuscateClientIdentification(String value) { + if (value.length() < 7) { + return value; + } + + return value.charAt(0) + "***" + value.substring(value.length() - 3); + } + + private Set toRoles(String roleCsv) { + if (StringUtils.isNotBlank(roleCsv)) { + return Set.of(roleCsv.split(",")); + } + return Set.of(); + } + +} From 318db865ebd34e8263d9a1f3a7437bc3c848bead Mon Sep 17 00:00:00 2001 From: Paulo Gomes da Cruz Junior Date: Tue, 17 Dec 2024 09:44:58 -0800 Subject: [PATCH 08/22] feat(FSADT1-1631): added location and contact load --- .../ForestClientContactRepository.java | 2 ++ .../ForestClientLocationRepository.java | 2 ++ .../gov/app/service/ClientSearchService.java | 20 +++++++++++++++++++ 3 files changed, 24 insertions(+) diff --git a/legacy/src/main/java/ca/bc/gov/app/repository/ForestClientContactRepository.java b/legacy/src/main/java/ca/bc/gov/app/repository/ForestClientContactRepository.java index a1bf7b0a54..6f73c92f8d 100644 --- a/legacy/src/main/java/ca/bc/gov/app/repository/ForestClientContactRepository.java +++ b/legacy/src/main/java/ca/bc/gov/app/repository/ForestClientContactRepository.java @@ -49,4 +49,6 @@ Flux matchByExpanded( String fax ); + Flux findAllByClientNumber(String clientNumber); + } diff --git a/legacy/src/main/java/ca/bc/gov/app/repository/ForestClientLocationRepository.java b/legacy/src/main/java/ca/bc/gov/app/repository/ForestClientLocationRepository.java index b7be401e5b..4ddef79860 100644 --- a/legacy/src/main/java/ca/bc/gov/app/repository/ForestClientLocationRepository.java +++ b/legacy/src/main/java/ca/bc/gov/app/repository/ForestClientLocationRepository.java @@ -44,4 +44,6 @@ Flux matchaddress( String country ); + Flux findAllByClientNumber(String clientNumber); + } diff --git a/legacy/src/main/java/ca/bc/gov/app/service/ClientSearchService.java b/legacy/src/main/java/ca/bc/gov/app/service/ClientSearchService.java index 4c6b54c296..a5e2cf18f5 100644 --- a/legacy/src/main/java/ca/bc/gov/app/service/ClientSearchService.java +++ b/legacy/src/main/java/ca/bc/gov/app/service/ClientSearchService.java @@ -6,8 +6,10 @@ import ca.bc.gov.app.configuration.ForestClientConfiguration; import ca.bc.gov.app.dto.AddressSearchDto; import ca.bc.gov.app.dto.ContactSearchDto; +import ca.bc.gov.app.dto.ForestClientContactDto; import ca.bc.gov.app.dto.ForestClientDetailsDto; import ca.bc.gov.app.dto.ForestClientDto; +import ca.bc.gov.app.dto.ForestClientLocationDto; import ca.bc.gov.app.dto.PredictiveSearchResultDto; import ca.bc.gov.app.entity.ClientDoingBusinessAsEntity; import ca.bc.gov.app.entity.ForestClientContactEntity; @@ -61,6 +63,8 @@ public class ClientSearchService { private final ForestClientContactRepository contactRepository; private final ForestClientLocationRepository locationRepository; private final AbstractForestClientMapper forestClientMapper; + private final AbstractForestClientMapper locationMapper; + private final AbstractForestClientMapper contactMapper; private final R2dbcEntityTemplate template; private final ForestClientConfiguration configuration; @@ -549,6 +553,22 @@ public Mono findByClientNumber(String clientNumber) { } return forestClientRepository.findDetailsByClientNumber(clientNumber) + .flatMap(dto -> + locationRepository + .findAllByClientNumber(clientNumber) + .map(locationMapper::toDto) + .collectList() + .map(dto::withAddresses) + .defaultIfEmpty(dto) + ) + .flatMap(dto -> + contactRepository + .findAllByClientNumber(clientNumber) + .map(contactMapper::toDto) + .collectList() + .map(dto::withContacts) + .defaultIfEmpty(dto) + ) .switchIfEmpty( Mono.error( new NoValueFoundException("client with number: " + clientNumber) From 200e0f44da2646f56034f14dea2c5ebbcdc05692 Mon Sep 17 00:00:00 2001 From: Paulo Gomes da Cruz Junior Date: Tue, 17 Dec 2024 13:48:58 -0800 Subject: [PATCH 09/22] test(FSADT1-1631): added tests --- ...ClientSearchControllerIntegrationTest.java | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/legacy/src/test/java/ca/bc/gov/app/controller/ClientSearchControllerIntegrationTest.java b/legacy/src/test/java/ca/bc/gov/app/controller/ClientSearchControllerIntegrationTest.java index 9bc6fdf0ac..b8287b83c9 100644 --- a/legacy/src/test/java/ca/bc/gov/app/controller/ClientSearchControllerIntegrationTest.java +++ b/legacy/src/test/java/ca/bc/gov/app/controller/ClientSearchControllerIntegrationTest.java @@ -586,20 +586,13 @@ private static Stream byPredictive() { @DisplayName("Search client by client number and groups") void shouldFindByClientNumber( String clientNumber, - List groups, String expectedClientNumber, Class exception ) { ResponseSpec response = client .get() - .uri(uriBuilder -> - uriBuilder - .path("/api/search/clientNumber") - .queryParam("clientNumber", clientNumber) - .queryParam("groups", String.join(",", groups)) - .build(new HashMap<>()) - ) + .uri("/api/search/clientNumber/{clientNumber}", clientNumber) .header("Content-Type", MediaType.APPLICATION_JSON_VALUE) .exchange(); @@ -620,17 +613,13 @@ void shouldFindByClientNumber( private static Stream byClientNumber() { return Stream.of( // Valid case - Arguments.of("00000123", List.of("CLIENT_ADMIN"), "00000123", null), + Arguments.of("00000138", "00000138", null), // Invalid case: missing client number - Arguments.of(null, List.of("CLIENT_ADMIN"), null, - MissingRequiredParameterException.class), - - // Invalid case: missing groups - Arguments.of("00000123", List.of(), null, MissingRequiredParameterException.class), + Arguments.of(null, null, MissingRequiredParameterException.class), // Invalid case: client not found - Arguments.of("99999999", List.of("CLIENT_ADMIN"), null, NoValueFoundException.class)); + Arguments.of("99999999", null, NoValueFoundException.class)); } } From 0c52c1bcb6d166fdb0b581538c53b77feb0af8fb Mon Sep 17 00:00:00 2001 From: Paulo Gomes da Cruz Junior Date: Tue, 17 Dec 2024 13:49:38 -0800 Subject: [PATCH 10/22] chore: removed print --- backend/src/main/java/ca/bc/gov/app/util/JwtPrincipalUtil.java | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/main/java/ca/bc/gov/app/util/JwtPrincipalUtil.java b/backend/src/main/java/ca/bc/gov/app/util/JwtPrincipalUtil.java index d3b1192948..8f73d59f3f 100644 --- a/backend/src/main/java/ca/bc/gov/app/util/JwtPrincipalUtil.java +++ b/backend/src/main/java/ca/bc/gov/app/util/JwtPrincipalUtil.java @@ -240,7 +240,6 @@ public static Set getGroups(Jwt jwtPrincipal) { private static Set getClaimGroups(Map tokenAttributes) { Object groups = tokenAttributes.get("cognito:groups"); - System.out.println(groups); if (groups instanceof List) { return ((List) groups).stream() From fadce8fd96170cfb033520090652672654be08c6 Mon Sep 17 00:00:00 2001 From: Maria Martinez Date: Wed, 18 Dec 2024 09:30:33 -0800 Subject: [PATCH 11/22] Doing code reviews --- .../app/service/bcregistry/BcRegistryService.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/backend/src/main/java/ca/bc/gov/app/service/bcregistry/BcRegistryService.java b/backend/src/main/java/ca/bc/gov/app/service/bcregistry/BcRegistryService.java index 2e8fb32c52..a5afa22c71 100644 --- a/backend/src/main/java/ca/bc/gov/app/service/bcregistry/BcRegistryService.java +++ b/backend/src/main/java/ca/bc/gov/app/service/bcregistry/BcRegistryService.java @@ -72,14 +72,14 @@ public BcRegistryService( * @throws InvalidAccessTokenException if the access token is invalid or expired */ public Flux searchByFacets(String name, String identifier) { - log.info("Searching BC Registry for {}", Objects.toString(name,identifier)); + log.info("Searching BC Registry for {}", Objects.toString(name, identifier)); return bcRegistryApi .post() .uri("/registry-search/api/v2/search/businesses") .body(BodyInserters.fromValue( new BcRegistryFacetRequestBodyDto( - new BcRegistryFacetRequestQueryDto(Objects.toString(name,identifier), name, identifier), + new BcRegistryFacetRequestQueryDto(Objects.toString(name, identifier), name, identifier), Map.of("status", List.of("ACTIVE")), 100, 0 @@ -119,8 +119,9 @@ public Flux searchByFacets(String name, Str .flatMapIterable(BcRegistryFacetSearchResultsDto::results) .filter(entry -> entry.status().equalsIgnoreCase("active")) .doOnNext( - content -> log.info("Found entry on BC Registry [{}] {}", content.identifier(), - content.name())); + content -> log.info("Found entry on BC Registry [{}] {}", + content.identifier(), + content.name())); } /** @@ -161,8 +162,9 @@ public Flux requestDocumentData(String value) { .bodyToMono(BcRegistryExceptionMessageDto.class) .map(BcRegistryExceptionMessageDto::rootCause) .doOnNext( - message -> log.error("Error while requesting data for {} -- {}", value, - message)) + message -> log.error("Error while requesting data for {} -- {}", + value, + message)) .filter(message -> message.contains("not found")) .switchIfEmpty(Mono.error(new InvalidAccessTokenException())) .flatMap(message -> Mono.error(new NoClientDataFound(value))) From 656533e79eaa0aef457644dd231c473446a01d2d Mon Sep 17 00:00:00 2001 From: Maria Martinez Date: Wed, 18 Dec 2024 09:34:12 -0800 Subject: [PATCH 12/22] Doing code reviews --- .../main/java/ca/bc/gov/app/service/ClientSearchService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/legacy/src/main/java/ca/bc/gov/app/service/ClientSearchService.java b/legacy/src/main/java/ca/bc/gov/app/service/ClientSearchService.java index a5e2cf18f5..1c08ec53a8 100644 --- a/legacy/src/main/java/ca/bc/gov/app/service/ClientSearchService.java +++ b/legacy/src/main/java/ca/bc/gov/app/service/ClientSearchService.java @@ -571,7 +571,7 @@ public Mono findByClientNumber(String clientNumber) { ) .switchIfEmpty( Mono.error( - new NoValueFoundException("client with number: " + clientNumber) + new NoValueFoundException("Client with number: " + clientNumber) ) ) .doOnNext( From 983bf40f56370ea2f21c0530af92bf89942d9e2e Mon Sep 17 00:00:00 2001 From: Paulo Gomes da Cruz Junior Date: Wed, 18 Dec 2024 10:18:37 -0800 Subject: [PATCH 13/22] chore: fixing discrepancies --- .../ca/bc/gov/app/converters/ForestClientObfuscate.java | 7 ++++++- .../service/client/ClientLegacyServiceIntegrationTest.java | 5 +---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/backend/src/main/java/ca/bc/gov/app/converters/ForestClientObfuscate.java b/backend/src/main/java/ca/bc/gov/app/converters/ForestClientObfuscate.java index 9b2c014232..3b2f8f8dc9 100644 --- a/backend/src/main/java/ca/bc/gov/app/converters/ForestClientObfuscate.java +++ b/backend/src/main/java/ca/bc/gov/app/converters/ForestClientObfuscate.java @@ -81,7 +81,12 @@ public void serialize( private String obfuscate(String propName, String propType, Object value) { Set roles = toRoles(MDC.get(ApplicationConstant.MDC_USERROLES)); - if (!roles.contains(ApplicationConstant.ROLE_VIEWER)) { + // Admins and Editors can see with no restrictions + if ( + roles.contains(ApplicationConstant.ROLE_EDITOR) + || roles.contains(ApplicationConstant.ROLE_ADMIN) + || roles.contains(ApplicationConstant.ROLE_SUSPEND) + ) { return value.toString(); } diff --git a/backend/src/test/java/ca/bc/gov/app/service/client/ClientLegacyServiceIntegrationTest.java b/backend/src/test/java/ca/bc/gov/app/service/client/ClientLegacyServiceIntegrationTest.java index 662561af39..5728b94434 100644 --- a/backend/src/test/java/ca/bc/gov/app/service/client/ClientLegacyServiceIntegrationTest.java +++ b/backend/src/test/java/ca/bc/gov/app/service/client/ClientLegacyServiceIntegrationTest.java @@ -152,7 +152,6 @@ private static Stream>> invalidValuesForMap() { @DisplayName("searching legacy by client number") void shouldSearchLegacyByClientNumber() { String clientNumber = "00000001"; - List groups = List.of("CLIENT_ADMIN"); ForestClientDetailsDto expectedDto = new ForestClientDetailsDto( clientNumber, @@ -182,9 +181,7 @@ void shouldSearchLegacyByClientNumber() { legacyStub .stubFor( - get(urlPathEqualTo("/api/search/clientNumber")) - .withQueryParam("clientNumber", equalTo(clientNumber)) - .withQueryParam("groups", equalTo("CLIENT_ADMIN")) + get(urlPathEqualTo("/api/search/clientNumber/" + clientNumber)) .willReturn(okJson("{" + "\"clientNumber\":\"00000001\"," + "\"clientName\":\"MY COMPANY LTD.\"," From 6e2cf937bf165834ebf31f1c0b64acce6de246d5 Mon Sep 17 00:00:00 2001 From: Paulo Gomes da Cruz Junior Date: Wed, 18 Dec 2024 11:23:43 -0800 Subject: [PATCH 14/22] chore: fixing sonar issues --- .../controller/ClientSearchController.java | 105 ++++++++++++++++-- 1 file changed, 98 insertions(+), 7 deletions(-) diff --git a/legacy/src/main/java/ca/bc/gov/app/controller/ClientSearchController.java b/legacy/src/main/java/ca/bc/gov/app/controller/ClientSearchController.java index b4f058c01c..9692e4864c 100644 --- a/legacy/src/main/java/ca/bc/gov/app/controller/ClientSearchController.java +++ b/legacy/src/main/java/ca/bc/gov/app/controller/ClientSearchController.java @@ -36,6 +36,13 @@ public class ClientSearchController { private final ClientSearchService service; + /** + * Handles the HTTP GET request to search for clients by registration number or company name. + * + * @param registrationNumber the registration number to search for (optional) + * @param companyName the company name to search for (optional) + * @return a Flux containing the matching ForestClientDto objects + */ @GetMapping("/registrationOrName") public Flux findByIncorporationOrName( @RequestParam(required = false) String registrationNumber, @@ -47,6 +54,16 @@ public Flux findByIncorporationOrName( .findByRegistrationNumberOrCompanyName(registrationNumber, companyName); } + /** + * Handles the HTTP GET request to search for individuals by their first name, last name, date of + * birth, and optional identification. + * + * @param firstName the first name of the individual to search for + * @param lastName the last name of the individual to search for + * @param dob the date of birth of the individual to search for + * @param identification the optional identification of the individual to search for + * @return a Flux containing the matching ForestClientDto objects + */ @GetMapping("/individual") public Flux findIndividuals( @RequestParam String firstName, @@ -59,14 +76,27 @@ public Flux findIndividuals( return service.findByIndividual(firstName, lastName, dob, identification, true); } + /** + * Handles the HTTP GET request to perform a fuzzy match search by company name. + * + * @param companyName the company name to search for + * @return a Flux containing the matching ForestClientDto objects + */ @GetMapping("/match") public Flux matchBy( @RequestParam String companyName ) { - log.info("Receiving request to match by company name {}", companyName); + log.info("Receiving request to fuzzy match by company name {}", companyName); return service.matchBy(companyName); } + /** + * Handles the HTTP GET request to search for clients by client ID and last name. + * + * @param clientId the client ID to search for + * @param lastName the last name to search for + * @return a Flux containing the matching ForestClientDto objects + */ @GetMapping("/idAndLastName") public Flux findByIdAndLastName( @RequestParam String clientId, @@ -75,19 +105,26 @@ public Flux findByIdAndLastName( log.info("Receiving request to search by ID {} and Last Name {}", clientId, lastName); return service.findByIdAndLastName(clientId, lastName); } - + /** * Handles the HTTP GET request to retrieve client details by client number. - * + * * @param clientNumber the client number to search for * @return a Mono containing the client details if found, or an empty Mono if not found */ @GetMapping("/clientNumber/{clientNumber}") public Mono findByClientNumber(@PathVariable String clientNumber) { - log.info("Receiving request to search by ID {}",clientNumber); + log.info("Receiving request to search by ID {}", clientNumber); return service.findByClientNumber(clientNumber); } + /** + * Handles the HTTP GET request to search for clients by identification type and value. + * + * @param idType the type of identification to search for + * @param identification the identification value to search for + * @return a Flux containing the matching ForestClientDto objects + */ @GetMapping("/id/{idType}/{identification}") public Flux findByIdentification( @PathVariable String idType, @@ -97,6 +134,12 @@ public Flux findByIdentification( return service.findByIdentification(idType, identification); } + /** + * Handles the HTTP GET request to search for clients by email. + * + * @param email the email address to search for + * @return a Flux containing the matching ForestClientDto objects + */ @GetMapping("/email") public Flux findByGeneralEmail( @RequestParam String email @@ -105,6 +148,12 @@ public Flux findByGeneralEmail( return service.findByGeneralEmail(email); } + /** + * Handles the HTTP GET request to search for clients by phone number. + * + * @param phone the phone number to search for + * @return a Flux containing the matching ForestClientDto objects + */ @GetMapping("/phone") public Flux findByGeneralPhone( @RequestParam String phone @@ -113,6 +162,12 @@ public Flux findByGeneralPhone( return service.findByGeneralPhoneNumber(phone); } + /** + * Handles the HTTP POST request to search for clients by address. + * + * @param address the address to search for + * @return a Flux containing the matching ForestClientDto objects + */ @PostMapping("/address") public Flux findByLocation( @RequestBody AddressSearchDto address @@ -121,6 +176,12 @@ public Flux findByLocation( return service.findByEntireAddress(address); } + /** + * Handles the HTTP POST request to search for clients by contact information. + * + * @param contact the contact information to search for + * @return a Flux containing the matching ForestClientDto objects + */ @PostMapping("/contact") public Flux findByContact( @RequestBody ContactSearchDto contact @@ -129,6 +190,12 @@ public Flux findByContact( return service.findByContact(contact); } + /** + * Handles the HTTP GET request to search for clients by acronym. + * + * @param acronym the acronym to search for + * @return a Flux containing the matching ForestClientDto objects + */ @GetMapping("/acronym") public Flux findByAcronym( @RequestParam String acronym @@ -137,10 +204,17 @@ public Flux findByAcronym( return service.findByAcronym(acronym); } + /** + * Handles the HTTP GET request to search for clients by doing business as (DBA) name. + * + * @param dbaName the DBA name to search for + * @param isFuzzy whether to perform a fuzzy match (default is true) + * @return a Flux containing the matching ForestClientDto objects + */ @GetMapping("/doingBusinessAs") public Flux findByDoingBusinessAs( @RequestParam String dbaName, - @RequestParam(required = false,defaultValue = "true") Boolean isFuzzy + @RequestParam(required = false, defaultValue = "true") Boolean isFuzzy ) { log.info("Receiving request to search by doing business as name {} being a {} match", dbaName, BooleanUtils.toString(isFuzzy, "fuzzy", "full") @@ -148,6 +222,12 @@ public Flux findByDoingBusinessAs( return service.findByDoingBusinessAs(dbaName, isFuzzy); } + /** + * Handles the HTTP GET request to search for clients by client name. + * + * @param clientName the client name to search for + * @return a Flux containing the matching ForestClientDto objects + */ @GetMapping("/clientName") public Flux findByClientName( @RequestParam String clientName @@ -155,7 +235,18 @@ public Flux findByClientName( log.info("Receiving request to match by company name {}", clientName); return service.findByClientName(clientName); } - + + /** + * Handles the HTTP GET request to perform a complex search or retrieve the latest entries. If a + * value is provided, it performs a complex search; otherwise, it retrieves the latest entries. + * The total count of results is added to the response headers. + * + * @param value the search value (optional) + * @param page the page number for pagination (default is 0) + * @param size the page size for pagination (default is 5) + * @param serverResponse the server response to add headers to + * @return a Flux containing the matching PredictiveSearchResultDto objects + */ @GetMapping public Flux findByComplexSearch( @RequestParam(required = false) String value, @@ -180,5 +271,5 @@ public Flux findByComplexSearch( .map(Pair::getKey); } } - + } From 0c50de163e879f008658b7a4b19003f09917c6be Mon Sep 17 00:00:00 2001 From: Paulo Gomes da Cruz Junior Date: Wed, 18 Dec 2024 11:31:05 -0800 Subject: [PATCH 15/22] chore: fixing sonar issues --- .../ForestClientContactRepository.java | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/legacy/src/main/java/ca/bc/gov/app/repository/ForestClientContactRepository.java b/legacy/src/main/java/ca/bc/gov/app/repository/ForestClientContactRepository.java index 6f73c92f8d..7b4e521e71 100644 --- a/legacy/src/main/java/ca/bc/gov/app/repository/ForestClientContactRepository.java +++ b/legacy/src/main/java/ca/bc/gov/app/repository/ForestClientContactRepository.java @@ -4,13 +4,21 @@ import org.springframework.data.r2dbc.repository.Query; import org.springframework.data.repository.reactive.ReactiveCrudRepository; import org.springframework.stereotype.Repository; - import reactor.core.publisher.Flux; + @Repository public interface ForestClientContactRepository - extends ReactiveCrudRepository { + extends ReactiveCrudRepository { + /** + * Finds client contacts by matching the contact name, business phone, or email address. + * + * @param contactName the contact name to match + * @param email the email address to match + * @param businessPhone the business phone number to match + * @return a Flux containing the matching ForestClientContactEntity objects + */ @Query(""" SELECT * FROM THE.CLIENT_CONTACT @@ -24,6 +32,17 @@ Flux matchBy( String businessPhone ); + /** + * Finds client contacts by matching the contact name, email address, and any of the provided + * phone numbers. + * + * @param contactName the contact name to match + * @param email the email address to match + * @param businessPhone the business phone number to match + * @param cellPhone the cell phone number to match + * @param fax the fax number to match + * @return a Flux containing the matching ForestClientContactEntity objects + */ @Query(""" SELECT * FROM THE.CLIENT_CONTACT @@ -49,6 +68,12 @@ Flux matchByExpanded( String fax ); + /** + * Finds all client contacts by client number. + * + * @param clientNumber the client number to match + * @return a Flux containing the matching ForestClientContactEntity objects + */ Flux findAllByClientNumber(String clientNumber); } From 5da9dbea06f40fa130e2a0ef9362a8c443122c66 Mon Sep 17 00:00:00 2001 From: Paulo Gomes da Cruz Junior Date: Wed, 18 Dec 2024 11:32:28 -0800 Subject: [PATCH 16/22] chore: fixing sonar issues --- .../ForestClientLocationRepository.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/legacy/src/main/java/ca/bc/gov/app/repository/ForestClientLocationRepository.java b/legacy/src/main/java/ca/bc/gov/app/repository/ForestClientLocationRepository.java index 4ddef79860..8449e7ae2d 100644 --- a/legacy/src/main/java/ca/bc/gov/app/repository/ForestClientLocationRepository.java +++ b/legacy/src/main/java/ca/bc/gov/app/repository/ForestClientLocationRepository.java @@ -11,6 +11,13 @@ public interface ForestClientLocationRepository extends ReactiveCrudRepository { + /** + * Finds client locations by matching the address and postal code. + * + * @param address the address to match + * @param postalCode the postal code to match + * @return a Flux containing the matching ForestClientLocationEntity objects + */ @Query(""" select * from the.client_location @@ -23,6 +30,16 @@ or upper(replace(replace(address_3,'-',' '),' ',' ')) like upper(replace(repla and upper(postal_code) = upper(replace(:postalCode, ' ', ''))""") Flux matchBy(String address, String postalCode); + /** + * Finds client locations by matching the address, city, province, country, and postal code. + * + * @param address the address to match + * @param postalCode the postal code to match + * @param city the city to match + * @param province the province to match + * @param country the country to match + * @return a Flux containing the matching ForestClientLocationEntity objects + */ @Query(""" select * from the.client_location @@ -44,6 +61,12 @@ Flux matchaddress( String country ); + /** + * Finds all client locations by client number. + * + * @param clientNumber the client number to match + * @return a Flux containing the matching ForestClientLocationEntity objects + */ Flux findAllByClientNumber(String clientNumber); } From abf26b24252d7c437b4997523b42a4ca3dad0091 Mon Sep 17 00:00:00 2001 From: Paulo Gomes da Cruz Junior Date: Wed, 18 Dec 2024 12:59:21 -0800 Subject: [PATCH 17/22] chore: fixing sonar issues --- .../gov/app/service/ClientSearchService.java | 81 +++++++++++++++---- 1 file changed, 65 insertions(+), 16 deletions(-) diff --git a/legacy/src/main/java/ca/bc/gov/app/service/ClientSearchService.java b/legacy/src/main/java/ca/bc/gov/app/service/ClientSearchService.java index 1c08ec53a8..99ed2b0dd0 100644 --- a/legacy/src/main/java/ca/bc/gov/app/service/ClientSearchService.java +++ b/legacy/src/main/java/ca/bc/gov/app/service/ClientSearchService.java @@ -26,7 +26,6 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.util.Comparator; -import java.util.List; import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -41,7 +40,6 @@ import org.springframework.data.relational.core.query.Criteria; import org.springframework.data.relational.core.query.Query; import org.springframework.stereotype.Service; -import org.springframework.util.CollectionUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -57,14 +55,18 @@ @Observed public class ClientSearchService { + public static final String CLIENT_NAME = "clientName"; + public static final String CLIENT_IDENTIFICATION = "clientIdentification"; private final ForestClientRepository forestClientRepository; private final ClientDoingBusinessAsRepository doingBusinessAsRepository; private final ForestClientRepository clientRepository; private final ForestClientContactRepository contactRepository; private final ForestClientLocationRepository locationRepository; private final AbstractForestClientMapper forestClientMapper; - private final AbstractForestClientMapper locationMapper; - private final AbstractForestClientMapper contactMapper; + private final + AbstractForestClientMapper locationMapper; + private final + AbstractForestClientMapper contactMapper; private final R2dbcEntityTemplate template; private final ForestClientConfiguration configuration; @@ -164,13 +166,13 @@ public Flux findByIndividual( } Criteria queryCriteria = where("legalFirstName").is(firstName).ignoreCase(true) - .and("clientName").is(lastName).ignoreCase(true) + .and(CLIENT_NAME).is(lastName).ignoreCase(true) .and("birthdate").is(dob.atStartOfDay()) .and("clientTypeCode").is("I").ignoreCase(true); if (StringUtils.isNotBlank(identification)) { queryCriteria = queryCriteria - .and("clientIdentification") + .and(CLIENT_IDENTIFICATION) .is(identification) .ignoreCase(true); } @@ -227,8 +229,8 @@ public Flux findByIdAndLastName(String clientId, String lastNam return Flux.error(new MissingRequiredParameterException("clientId, lastName")); } - Criteria queryCriteria = where("clientIdentification").is(clientId).ignoreCase(true) - .and("clientName").is(lastName).ignoreCase(true); + Criteria queryCriteria = where(CLIENT_IDENTIFICATION).is(clientId).ignoreCase(true) + .and(CLIENT_NAME).is(lastName).ignoreCase(true); return searchClientByQuery(queryCriteria, ForestClientEntity.class) .map(forestClientMapper::toDto) @@ -260,7 +262,7 @@ public Flux findByIdentification(String idType, String identifi } Criteria queryCriteria = where("clientIdTypeCode").is(idType).ignoreCase(true) - .and("clientIdentification").is(identification).ignoreCase(true) + .and(CLIENT_IDENTIFICATION).is(identification).ignoreCase(true) .and("clientTypeCode").is("I").ignoreCase(true); return searchClientByQuery(queryCriteria, ForestClientEntity.class) @@ -467,6 +469,13 @@ public Flux findByContact(ContactSearchDto contact) { ); } + /** + * Finds clients by their acronym. Logs the search process and results. If the acronym is blank, + * returns a MissingRequiredParameterException. + * + * @param acronym the acronym to search for + * @return a Flux containing the matching ForestClientDto objects + */ public Flux findByAcronym(String acronym) { log.info("Searching for client with acronym {}", acronym); @@ -487,6 +496,15 @@ public Flux findByAcronym(String acronym) { ); } + /** + * Finds clients by their doing business as (DBA) name. Logs the search process and results. If + * the DBA name is blank, returns a MissingRequiredParameterException. If the isFuzzy parameter is + * true, performs a fuzzy search. + * + * @param doingBusinessAs the DBA name to search for + * @param isFuzzy whether to perform a fuzzy search + * @return a Flux containing the matching ForestClientDto objects + */ public Flux findByDoingBusinessAs(String doingBusinessAs, boolean isFuzzy) { if (StringUtils.isBlank(doingBusinessAs)) { @@ -521,18 +539,23 @@ public Flux findByDoingBusinessAs(String doingBusinessAs, boole doingBusinessAs, dto.clientNumber(), dto.clientName()) ); - - } + /** + * Finds clients by their name. Logs the search process and results. If the client name is blank, + * returns a MissingRequiredParameterException. + * + * @param clientName the name of the client to search for + * @return a Flux containing the matching ForestClientDto objects + */ public Flux findByClientName(String clientName) { log.info("Searching for client with name {}", clientName); if (StringUtils.isBlank(clientName)) { - return Flux.error(new MissingRequiredParameterException("clientName")); + return Flux.error(new MissingRequiredParameterException(CLIENT_NAME)); } - Criteria queryCriteria = where("clientName").is(clientName).ignoreCase(true); + Criteria queryCriteria = where(CLIENT_NAME).is(clientName).ignoreCase(true); return searchClientByQuery(queryCriteria, ForestClientEntity.class) .map(forestClientMapper::toDto) @@ -544,7 +567,15 @@ public Flux findByClientName(String clientName) { dto.clientNumber(), dto.clientName()) ); } - + + /** + * Finds client details by their client number. Logs the search process and results. If the client + * number is blank, returns a MissingRequiredParameterException. If no client is found, returns a + * NoValueFoundException. + * + * @param clientNumber the client number to search for + * @return a Mono containing the matching ForestClientDetailsDto object + */ public Mono findByClientNumber(String clientNumber) { log.info("Searching for client with number {}", clientNumber); @@ -560,7 +591,7 @@ public Mono findByClientNumber(String clientNumber) { .collectList() .map(dto::withAddresses) .defaultIfEmpty(dto) - ) + ) .flatMap(dto -> contactRepository .findAllByClientNumber(clientNumber) @@ -568,7 +599,7 @@ public Mono findByClientNumber(String clientNumber) { .collectList() .map(dto::withContacts) .defaultIfEmpty(dto) - ) + ) .switchIfEmpty( Mono.error( new NoValueFoundException("Client with number: " + clientNumber) @@ -581,6 +612,16 @@ public Mono findByClientNumber(String clientNumber) { ); } + /** + * Performs a complex search for clients based on a predictive search value. Logs the search + * process and results. If the search value is blank, returns a + * MissingRequiredParameterException. + * + * @param value the predictive search value + * @param page the pagination information + * @return a Flux containing pairs of PredictiveSearchResultDto objects and the total count of + * matching clients + */ public Flux> complexSearch(String value, Pageable page) { // This condition is for predictive search, and we will stop here if no query param is provided if (StringUtils.isBlank(value)) { @@ -601,6 +642,14 @@ public Flux> complexSearch(String value, P ); } + /** + * Retrieves the latest entries of predictive search results. Logs the search process and + * results. + * + * @param page the pagination information + * @return a Flux containing pairs of PredictiveSearchResultDto objects and the total count of + * matching clients + */ public Flux> latestEntries(Pageable page) { return forestClientRepository From 9952ec3b9338c963772b7799c8a6402d7e9fd8fc Mon Sep 17 00:00:00 2001 From: Paulo Gomes da Cruz Junior Date: Wed, 18 Dec 2024 13:00:22 -0800 Subject: [PATCH 18/22] chore: fixing sonar issues --- ...ClientSearchControllerIntegrationTest.java | 61 +++++++++---------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/legacy/src/test/java/ca/bc/gov/app/controller/ClientSearchControllerIntegrationTest.java b/legacy/src/test/java/ca/bc/gov/app/controller/ClientSearchControllerIntegrationTest.java index b8287b83c9..b8b63d260d 100644 --- a/legacy/src/test/java/ca/bc/gov/app/controller/ClientSearchControllerIntegrationTest.java +++ b/legacy/src/test/java/ca/bc/gov/app/controller/ClientSearchControllerIntegrationTest.java @@ -6,7 +6,6 @@ import ca.bc.gov.app.exception.NoValueFoundException; import ca.bc.gov.app.extensions.AbstractTestContainerIntegrationTest; import java.util.HashMap; -import java.util.List; import java.util.Optional; import java.util.stream.Stream; import lombok.extern.slf4j.Slf4j; @@ -340,15 +339,15 @@ void shouldSearchEmpty() { .header("Content-Type", MediaType.APPLICATION_JSON_VALUE) .exchange(); - response - .expectStatus().isOk() - .expectHeader() - .exists("X-Total-Count") - .expectBody() - .jsonPath("$[0].clientNumber").isNotEmpty() - .jsonPath("$[0].clientName").isNotEmpty() - .jsonPath("$.length()").isEqualTo(10) - .consumeWith(System.out::println); + response + .expectStatus().isOk() + .expectHeader() + .exists("X-Total-Count") + .expectBody() + .jsonPath("$[0].clientNumber").isNotEmpty() + .jsonPath("$[0].clientName").isNotEmpty() + .jsonPath("$.length()").isEqualTo(10) + .consumeWith(System.out::println); } @@ -580,7 +579,7 @@ private static Stream byPredictive() { Arguments.of("matelda", null, null, "00000137", "MATELDA LINDHE (JABBERTYPE)") ); } - + @ParameterizedTest @MethodSource("byClientNumber") @DisplayName("Search client by client number and groups") @@ -589,27 +588,27 @@ void shouldFindByClientNumber( String expectedClientNumber, Class exception ) { - ResponseSpec response = - client - .get() - .uri("/api/search/clientNumber/{clientNumber}", clientNumber) - .header("Content-Type", MediaType.APPLICATION_JSON_VALUE) - .exchange(); - - if (StringUtils.isNotBlank(expectedClientNumber)) { - response - .expectStatus().isOk() - .expectBody() - .jsonPath("$.clientNumber").isNotEmpty() - .jsonPath("$.clientNumber").isEqualTo(expectedClientNumber) - .consumeWith(System.out::println); - } - - if (exception != null) { - response.expectStatus().is4xxClientError(); - } + ResponseSpec response = + client + .get() + .uri("/api/search/clientNumber/{clientNumber}", clientNumber) + .header("Content-Type", MediaType.APPLICATION_JSON_VALUE) + .exchange(); + + if (StringUtils.isNotBlank(expectedClientNumber)) { + response + .expectStatus().isOk() + .expectBody() + .jsonPath("$.clientNumber").isNotEmpty() + .jsonPath("$.clientNumber").isEqualTo(expectedClientNumber) + .consumeWith(System.out::println); + } + + if (exception != null) { + response.expectStatus().is4xxClientError(); + } } - + private static Stream byClientNumber() { return Stream.of( // Valid case From 270558652a2d975581446293070b4d48c3ee6bdf Mon Sep 17 00:00:00 2001 From: Paulo Gomes da Cruz Junior Date: Wed, 18 Dec 2024 13:32:24 -0800 Subject: [PATCH 19/22] chore: fixing sonar issues --- .../ForestClientContactRepository.java | 7 ++++++- .../ForestClientLocationRepository.java | 21 +++++++++++++------ .../gov/app/service/ClientSearchService.java | 10 ++++----- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/legacy/src/main/java/ca/bc/gov/app/repository/ForestClientContactRepository.java b/legacy/src/main/java/ca/bc/gov/app/repository/ForestClientContactRepository.java index 7b4e521e71..82daf02c88 100644 --- a/legacy/src/main/java/ca/bc/gov/app/repository/ForestClientContactRepository.java +++ b/legacy/src/main/java/ca/bc/gov/app/repository/ForestClientContactRepository.java @@ -6,7 +6,12 @@ import org.springframework.stereotype.Repository; import reactor.core.publisher.Flux; - +/** + * Repository for the ForestClientContactEntity. + * + *

Provides methods to query the client table in the database. The methods are used to find + * client by straight field comparison and fuzzy match

+ */ @Repository public interface ForestClientContactRepository extends ReactiveCrudRepository { diff --git a/legacy/src/main/java/ca/bc/gov/app/repository/ForestClientLocationRepository.java b/legacy/src/main/java/ca/bc/gov/app/repository/ForestClientLocationRepository.java index 8449e7ae2d..e5f9c0979a 100644 --- a/legacy/src/main/java/ca/bc/gov/app/repository/ForestClientLocationRepository.java +++ b/legacy/src/main/java/ca/bc/gov/app/repository/ForestClientLocationRepository.java @@ -7,6 +7,9 @@ import org.springframework.stereotype.Repository; import reactor.core.publisher.Flux; +/** + * Repository for the ForestClientLocationEntity. + */ @Repository public interface ForestClientLocationRepository extends ReactiveCrudRepository { @@ -23,9 +26,12 @@ public interface ForestClientLocationRepository from the.client_location where ( - upper(replace(replace(address_1,'-',' '),' ',' ')) like upper(replace(replace(concat(:address, '%'),'-',' '),' ',' ')) - or upper(replace(replace(address_2,'-',' '),' ',' ')) like upper(replace(replace(concat(:address, '%'),'-',' '),' ',' ')) - or upper(replace(replace(address_3,'-',' '),' ',' ')) like upper(replace(replace(concat(:address, '%'),'-',' '),' ',' ')) + upper(replace(replace(address_1,'-',' '),' ',' ')) + like upper(replace(replace(concat(:address, '%'),'-',' '),' ',' ')) + or upper(replace(replace(address_2,'-',' '),' ',' ')) + like upper(replace(replace(concat(:address, '%'),'-',' '),' ',' ')) + or upper(replace(replace(address_3,'-',' '),' ',' ')) + like upper(replace(replace(concat(:address, '%'),'-',' '),' ',' ')) ) and upper(postal_code) = upper(replace(:postalCode, ' ', ''))""") Flux matchBy(String address, String postalCode); @@ -45,9 +51,12 @@ and upper(postal_code) = upper(replace(:postalCode, ' ', ''))""") from the.client_location where ( - upper(replace(replace(address_1,'-',' '),' ',' ')) like upper(replace(replace(concat(:address, '%'),'-',' '),' ',' ')) - or upper(replace(replace(address_2,'-',' '),' ',' ')) like upper(replace(replace(concat(:address, '%'),'-',' '),' ',' ')) - or upper(replace(replace(address_3,'-',' '),' ',' ')) like upper(replace(replace(concat(:address, '%'),'-',' '),' ',' ')) + upper(replace(replace(address_1,'-',' '),' ',' ')) + like upper(replace(replace(concat(:address, '%'),'-',' '),' ',' ')) + or upper(replace(replace(address_2,'-',' '),' ',' ')) + like upper(replace(replace(concat(:address, '%'),'-',' '),' ',' ')) + or upper(replace(replace(address_3,'-',' '),' ',' ')) + like upper(replace(replace(concat(:address, '%'),'-',' '),' ',' ')) ) and upper(city) = upper(:city) and upper(province) = upper(:province) diff --git a/legacy/src/main/java/ca/bc/gov/app/service/ClientSearchService.java b/legacy/src/main/java/ca/bc/gov/app/service/ClientSearchService.java index 99ed2b0dd0..7d262524f9 100644 --- a/legacy/src/main/java/ca/bc/gov/app/service/ClientSearchService.java +++ b/legacy/src/main/java/ca/bc/gov/app/service/ClientSearchService.java @@ -432,9 +432,9 @@ public Flux findByEntireAddress(AddressSearchDto address) { * * @param contact The {@link ContactSearchDto} containing search criteria such as name, email, and * phone number. - * @return A {@link Flux} stream of client DTOs that match the search criteria. - * If the contact parameter is null or not valid, a {@link MissingRequiredParameterException} is - * emitted. + * @return A {@link Flux} of {@link ForestClientDto} stream of client DTOs that match the search + * criteria. If the contact parameter is null or not valid, a + * {@link MissingRequiredParameterException} is emitted. */ public Flux findByContact(ContactSearchDto contact) { @@ -620,7 +620,7 @@ public Mono findByClientNumber(String clientNumber) { * @param value the predictive search value * @param page the pagination information * @return a Flux containing pairs of PredictiveSearchResultDto objects and the total count of - * matching clients + * matching clients */ public Flux> complexSearch(String value, Pageable page) { // This condition is for predictive search, and we will stop here if no query param is provided @@ -648,7 +648,7 @@ public Flux> complexSearch(String value, P * * @param page the pagination information * @return a Flux containing pairs of PredictiveSearchResultDto objects and the total count of - * matching clients + * matching clients */ public Flux> latestEntries(Pageable page) { return From 6a6ae7b546d233b8e12f0cb6d5c60aa024e21f7b Mon Sep 17 00:00:00 2001 From: Paulo Gomes da Cruz Junior Date: Wed, 18 Dec 2024 13:55:49 -0800 Subject: [PATCH 20/22] chore: fixing discrepancies --- .../ca/bc/gov/app/ApplicationConstant.java | 47 +++++++++++--- .../GlobalServiceConfiguration.java | 33 ++++++++++ .../controller/client/ClientController.java | 52 ++++++++------- .../filters/ContextPropagatorWebFilter.java | 23 ++++--- .../controller/filters/UserIdWebFilter.java | 6 ++ .../filters/UserRolesWebFilter.java | 21 ++++++ ...ForestClientDetailsSerializerModifier.java | 15 ++++- .../app/converters/ForestClientObfuscate.java | 60 ++++++++++++----- .../dto/legacy/ForestClientDetailsDto.java | 29 ++++++++- .../service/bcregistry/BcRegistryService.java | 22 ++++--- .../service/client/ClientLegacyService.java | 65 ++++++++++++------- 11 files changed, 278 insertions(+), 95 deletions(-) diff --git a/backend/src/main/java/ca/bc/gov/app/ApplicationConstant.java b/backend/src/main/java/ca/bc/gov/app/ApplicationConstant.java index bac4f5843b..d2a81a69e3 100644 --- a/backend/src/main/java/ca/bc/gov/app/ApplicationConstant.java +++ b/backend/src/main/java/ca/bc/gov/app/ApplicationConstant.java @@ -54,12 +54,18 @@ public final class ApplicationConstant { dc.district_code as district, dc.district_code || ' - ' || dc.description as district_desc FROM nrfc.submission s - left join nrfc.submission_status_code ssc on ssc.submission_status_code = s.submission_status_code\s - left join nrfc.submission_type_code stc on stc.submission_type_code = s.submission_type_code - left join nrfc.submission_detail sd on sd.submission_id = s.submission_id\s - left join nrfc.business_type_code btc on btc.business_type_code = sd.business_type_code\s - left join nrfc.district_code dc on dc.district_code = sd.district_code\s - left join nrfc.client_type_code ctc on ctc.client_type_code = sd.client_type_code\s + left join nrfc.submission_status_code ssc + on ssc.submission_status_code = s.submission_status_code + left join nrfc.submission_type_code stc + on stc.submission_type_code = s.submission_type_code + left join nrfc.submission_detail sd + on sd.submission_id = s.submission_id + left join nrfc.business_type_code btc + on btc.business_type_code = sd.business_type_code + left join nrfc.district_code dc + on dc.district_code = sd.district_code + left join nrfc.client_type_code ctc + on ctc.client_type_code = sd.client_type_code where s.submission_id = :submissionId"""; public static final String SUBMISSION_CONTACTS_QUERY = """ @@ -71,7 +77,15 @@ public final class ApplicationConstant { sc.last_name, sc.business_phone_number, sc.email_address, - (select STRING_AGG(sl.location_name,', ') as locations from nrfc.submission_location sl left join nrfc.submission_location_contact_xref slcx on slcx.submission_location_id = sl.submission_location_id left join nrfc.submission_contact sc on sc.submission_contact_id = slcx.submission_contact_id where sl.submission_id = :submissionId) as locations, + ( + select STRING_AGG(sl.location_name,', ') as locations + from nrfc.submission_location sl + left join nrfc.submission_location_contact_xref slcx + on slcx.submission_location_id = sl.submission_location_id + left join nrfc.submission_contact sc + on sc.submission_contact_id = slcx.submission_contact_id + where sl.submission_id = :submissionId + ) as locations, sc.idp_user_id FROM nrfc.submission_contact sc left join nrfc.contact_type_code ctc on ctc.contact_type_code = sc.contact_type_code @@ -90,7 +104,9 @@ public final class ApplicationConstant { sl.location_name FROM nrfc.submission_location sl left join nrfc.country_code cc on cc.country_code = sl.country_code - left join nrfc.province_code pc on (pc.province_code = sl.province_code and pc.country_code = cc.country_code) + left join nrfc.province_code pc on ( + pc.province_code = sl.province_code and pc.country_code = cc.country_code + ) where sl.submission_id = :submissionId order by sl.submission_location_id"""; @@ -108,7 +124,20 @@ left join nrfc.province_code pc on (pc.province_code = sl.province_code and pc.c public static final String ROLE_ADMIN = "CLIENT_ADMIN"; public static final String ROLE_SUSPEND = "CLIENT_SUSPEND"; - public static final String OPENDATA_FILTER = "%s*%s*%s*%s*%s*%s*%s*%s*"; + public static final String OPENDATA_FILTER = "" + + "%s" + + "*%s*" + + "%s" + + "*%s*" + + "%s" + + "*%s*" + + "%s" + + "*%s*" + + ""; public static final String MDC_USERID = "X-USER"; diff --git a/backend/src/main/java/ca/bc/gov/app/configuration/GlobalServiceConfiguration.java b/backend/src/main/java/ca/bc/gov/app/configuration/GlobalServiceConfiguration.java index a00bebe9a7..dee5f4c20b 100644 --- a/backend/src/main/java/ca/bc/gov/app/configuration/GlobalServiceConfiguration.java +++ b/backend/src/main/java/ca/bc/gov/app/configuration/GlobalServiceConfiguration.java @@ -248,6 +248,15 @@ public WebClient openDataSacBandApi( .baseUrl(configuration.getOpenData().getSacBandUrl()).build(); } + /** + * Configures and provides a WebClient for accessing the Open Data SAC Tribe API. This WebClient + * is pre-configured with the base URL for the SAC Tribe API, as specified in the provided + * {@link ForestClientConfiguration}. + * + * @param configuration The configuration containing the SAC Tribe API URL and other settings. + * @param webClientBuilder A builder for creating WebClient instances. + * @return A WebClient instance configured for the Open Data SAC Tribe API. + */ @Bean public WebClient openDataSacTribeApi( ForestClientConfiguration configuration, @@ -275,6 +284,16 @@ public WebClient openDataBcMapsBandApi( return webClientBuilder.baseUrl(configuration.getOpenData().getOpenMapsBandUrl()).build(); } + /** + * Configures and provides a WebClient for accessing the Open Data BC Maps Tribe API. This + * WebClient is pre-configured with the base URL for the BC Maps Tribe API, as specified in the + * provided {@link ForestClientConfiguration}. + * + * @param configuration The configuration containing the BC Maps Tribe API URL and other + * settings. + * @param webClientBuilder A builder for creating WebClient instances. + * @return A WebClient instance configured for the Open Data BC Maps Tribe API. + */ @Bean public WebClient openDataBcMapsTribeApi( ForestClientConfiguration configuration, @@ -306,6 +325,14 @@ public WebClient processorApi( return webClientBuilder.baseUrl(configuration.getProcessor().getUrl()).build(); } + /** + * Configures and provides an ObjectMapper bean. This ObjectMapper is built using the provided + * Jackson2ObjectMapperBuilder and is configured with the JavaTimeModule and a custom + * ForestClientDetailsSerializerModifier module. + * + * @param builder The Jackson2ObjectMapperBuilder used to build the ObjectMapper. + * @return A configured ObjectMapper instance. + */ @Bean public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) { @@ -316,6 +343,12 @@ public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) { return mapper; } + /** + * Creates and configures a SimpleModule for customizing the serialization of ForestClientDetails. + * This module registers a custom serializer modifier, ForestClientDetailsSerializerModifier. + * + * @return A configured SimpleModule instance with the custom serializer modifier. + */ SimpleModule forestClientDetailsDtoModule() { SimpleModule module = new SimpleModule(); diff --git a/backend/src/main/java/ca/bc/gov/app/controller/client/ClientController.java b/backend/src/main/java/ca/bc/gov/app/controller/client/ClientController.java index 487f54246a..a08da51e62 100644 --- a/backend/src/main/java/ca/bc/gov/app/controller/client/ClientController.java +++ b/backend/src/main/java/ca/bc/gov/app/controller/client/ClientController.java @@ -44,7 +44,7 @@ public class ClientController { * provider) is extracted from the token to authorize the request. * * @param clientNumber the incorporation number of the client whose details are being requested - * @param principal the JWT authentication token containing user and business information + * @param principal the JWT authentication token containing user and business information * @return a {@link Mono} emitting the {@link ClientDetailsDto} containing the client's details */ @GetMapping("/{clientNumber}") @@ -66,21 +66,23 @@ public Mono getClientDetailsByIncorporationNumber( } @GetMapping("/details/{clientNumber}") - public Mono getClientDetailsByClientNumber(@PathVariable String clientNumber) { + public Mono getClientDetailsByClientNumber( + @PathVariable String clientNumber) { log.info("Requesting client details for client number {}", clientNumber); return clientService.getClientDetailsByClientNumber(clientNumber); } - + /** - * Performs a full-text search for clients based on the provided keyword, with pagination support. + * Performs a full-text search for clients based on the provided keyword, with pagination + * support. * - *

This endpoint allows searching for clients by a keyword. The results are paginated, and the + *

This endpoint allows searching for clients by a keyword. The results are paginated, and the * total count of matching records is included in the response headers. * - * @param page the page number to retrieve (default is 0) - * @param size the number of records per page (default is 10) - * @param keyword the keyword to search for (default is an empty string, which returns all - * records) + * @param page the page number to retrieve (default is 0) + * @param size the number of records per page (default is 10) + * @param keyword the keyword to search for (default is an empty string, which returns all + * records) * @param serverResponse the HTTP response to include the total count of records in the headers * @return a {@link Flux} emitting {@link ClientListDto} objects containing the search results */ @@ -107,15 +109,15 @@ public Flux fullSearch( }) .map(Pair::getFirst) .doFinally(signalType -> - serverResponse - .getHeaders() - .putIfAbsent( - ApplicationConstant.X_TOTAL_COUNT, - List.of("0") - ) + serverResponse + .getHeaders() + .putIfAbsent( + ApplicationConstant.X_TOTAL_COUNT, + List.of("0") + ) ); } - + /** * Retrieve a Flux of ClientLookUpDto objects by searching for clients with a specific name. * @@ -133,18 +135,18 @@ public Flux findByClientName(@PathVariable String name) { /** * Finds a client based on their registration number. * - *

This endpoint retrieves client information by searching for a registration number. + *

This endpoint retrieves client information by searching for a registration number. * If no client is found, an error is returned. * * @param registrationNumber the registration number of the client to look up - * @return a {@link Mono} emitting the {@link ClientLookUpDto} if found, or an error - * if no data exists + * @return a {@link Mono} emitting the {@link ClientLookUpDto} if found, or an error if no data + * exists */ @GetMapping(value = "/incorporation/{registrationNumber}") public Mono findByRegistrationNumber( @PathVariable String registrationNumber) { log.info("Requesting a client with registration number {} from the client service.", - registrationNumber); + registrationNumber); return clientService .findByClientNameOrIncorporation(registrationNumber) .next() @@ -154,10 +156,10 @@ public Mono findByRegistrationNumber( /** * Searches for an individual client by user ID and last name. * - *

This endpoint fetches an individual client using their user ID and last name. + *

This endpoint fetches an individual client using their user ID and last name. * The request is validated against existing records in the system. * - * @param userId the unique identifier of the individual to search for + * @param userId the unique identifier of the individual to search for * @param lastName the last name of the individual to search for * @return a {@link Mono} indicating completion, or an error if the individual is not found */ @@ -166,9 +168,9 @@ public Mono findByIndividual( @PathVariable String userId, @RequestParam String lastName ) { - log.info("Receiving request to search individual with id {} and last name {}", - userId, - lastName); + log.info("Receiving request to search individual with id {} and last name {}", + userId, + lastName); return clientService.findByIndividual(userId, lastName); } diff --git a/backend/src/main/java/ca/bc/gov/app/controller/filters/ContextPropagatorWebFilter.java b/backend/src/main/java/ca/bc/gov/app/controller/filters/ContextPropagatorWebFilter.java index 0ad9a92882..28c95347e1 100644 --- a/backend/src/main/java/ca/bc/gov/app/controller/filters/ContextPropagatorWebFilter.java +++ b/backend/src/main/java/ca/bc/gov/app/controller/filters/ContextPropagatorWebFilter.java @@ -12,6 +12,12 @@ import reactor.core.publisher.Mono; import reactor.util.context.Context; +/** + * This class is a web filter that extracts a value from the security context and sets it in the MDC + * (Mapped Diagnostic Context) for logging and tracing purposes. The value is then propagated down + * the filter chain and set in the reactive context to ensure it is available for logging and + * tracing in both reactive and non-reactive parts of the application. + */ public abstract class ContextPropagatorWebFilter implements WebFilter { protected abstract String getContextKey(); @@ -20,16 +26,15 @@ public abstract class ContextPropagatorWebFilter implements WebFilter { /** * Filters each incoming request to extract from the security context a {@link String} value using - * {@link #getContextValueExtractor()} and set it in the MDC. - * The value is then propagated down the filter chain and set in the reactive context - * to ensure it is available for logging and tracing in both reactive and non-reactive parts of - * the application. + * {@link #getContextValueExtractor()} and set it in the MDC. The value is then propagated down + * the filter chain and set in the reactive context to ensure it is available for logging and + * tracing in both reactive and non-reactive parts of the application. * * @param exchange The current server web exchange that contains information about the request and * response. * @param chain The web filter chain that allows the filter to pass on the request to the next * entity in the chain. - * @return A Mono that indicates when request handling is complete. + * @return A {@link Mono} of {@link Void} that indicates when request handling is complete. */ @Override public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { @@ -56,10 +61,10 @@ public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { /** * Initializes and registers a thread-local context for the current thread. This method configures * the {@link ContextRegistry} to handle the value within the MDC (Mapped Diagnostic Context), - * allowing for the propagation of the value across different parts of the application that run - * on the same thread. It specifically sets up accessors for getting, setting, and removing the - * value from the MDC. This setup is crucial for maintaining state across the reactive and - * non-reactive parts of the application. + * allowing for the propagation of the value across different parts of the application that run on + * the same thread. It specifically sets up accessors for getting, setting, and removing the value + * from the MDC. This setup is crucial for maintaining state across the reactive and non-reactive + * parts of the application. */ private void contextLoad() { ContextRegistry diff --git a/backend/src/main/java/ca/bc/gov/app/controller/filters/UserIdWebFilter.java b/backend/src/main/java/ca/bc/gov/app/controller/filters/UserIdWebFilter.java index 45875b43db..6927add080 100644 --- a/backend/src/main/java/ca/bc/gov/app/controller/filters/UserIdWebFilter.java +++ b/backend/src/main/java/ca/bc/gov/app/controller/filters/UserIdWebFilter.java @@ -24,6 +24,12 @@ @Order(-1) public class UserIdWebFilter extends ContextPropagatorWebFilter { + /** + * Retrieves the context key for the user ID. This key is used to store the user ID in the MDC + * (Mapped Diagnostic Context). + * + * @return The context key for the user ID. + */ @Override protected String getContextKey() { return ApplicationConstant.MDC_USERID; diff --git a/backend/src/main/java/ca/bc/gov/app/controller/filters/UserRolesWebFilter.java b/backend/src/main/java/ca/bc/gov/app/controller/filters/UserRolesWebFilter.java index 806948ccb2..faf94aba23 100644 --- a/backend/src/main/java/ca/bc/gov/app/controller/filters/UserRolesWebFilter.java +++ b/backend/src/main/java/ca/bc/gov/app/controller/filters/UserRolesWebFilter.java @@ -14,16 +14,37 @@ import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.springframework.stereotype.Component; +/** + * Extracts the user roles from the security context and sets them in the MDC (Mapped Diagnostic + * Context). This filter is applied globally to all incoming requests. It ensures that the user + * roles are available in the MDC, facilitating logging and tracing of requests by user roles. The + * filter operates in both reactive and non-reactive contexts, ensuring compatibility across + * different parts. + */ @Component @Slf4j @Order(-2) public class UserRolesWebFilter extends ContextPropagatorWebFilter { + /** + * Retrieves the context key for the user roles. This key is used to store the user roles in the + * MDC (Mapped Diagnostic Context). + * + * @return The context key for the user roles. + */ @Override protected String getContextKey() { return ApplicationConstant.MDC_USERROLES; } + /** + * Extracts the context value for the user roles from the given Authentication object. This + * function retrieves the roles from different types of principals (JwtAuthenticationToken, Jwt, + * UserDetails) and concatenates them into a comma-separated string. + * + * @return A function that takes an Authentication object and returns a comma-separated string of + * user roles. + */ @Override protected Function getContextValueExtractor() { return authentication -> { diff --git a/backend/src/main/java/ca/bc/gov/app/converters/ForestClientDetailsSerializerModifier.java b/backend/src/main/java/ca/bc/gov/app/converters/ForestClientDetailsSerializerModifier.java index 57746fd530..f99bb22ba2 100644 --- a/backend/src/main/java/ca/bc/gov/app/converters/ForestClientDetailsSerializerModifier.java +++ b/backend/src/main/java/ca/bc/gov/app/converters/ForestClientDetailsSerializerModifier.java @@ -7,9 +7,22 @@ import com.fasterxml.jackson.databind.ser.BeanSerializerModifier; import lombok.extern.slf4j.Slf4j; +/** + * A custom BeanSerializerModifier that modifies the serializer for ForestClientDetailsDto. + * If the bean class is assignable from ForestClientDetailsDto, it returns a custom serializer + * ForestClientObfuscate. Otherwise, it returns the default serializer. + */ @Slf4j public class ForestClientDetailsSerializerModifier extends BeanSerializerModifier { + /** + * Modifies the serializer for the given bean description. + * + * @param config The serialization configuration. + * @param beanDesc The bean description. + * @param serializer The default serializer. + * @return A custom serializer if the bean class is ForestClientDetailsDto, otherwise the default serializer. + */ @Override public JsonSerializer modifySerializer( SerializationConfig config, @@ -23,4 +36,4 @@ public JsonSerializer modifySerializer( return super.modifySerializer(config, beanDesc, serializer); } -} +} \ No newline at end of file diff --git a/backend/src/main/java/ca/bc/gov/app/converters/ForestClientObfuscate.java b/backend/src/main/java/ca/bc/gov/app/converters/ForestClientObfuscate.java index 3b2f8f8dc9..29b6bc7348 100644 --- a/backend/src/main/java/ca/bc/gov/app/converters/ForestClientObfuscate.java +++ b/backend/src/main/java/ca/bc/gov/app/converters/ForestClientObfuscate.java @@ -13,23 +13,20 @@ import org.apache.commons.lang3.StringUtils; import org.slf4j.MDC; -/** - * Requirement - ID - Suppress GIVEN Client detail summary edit mode WHEN in edit mode and role is - * CLIENT_VIEW THEN system will display the ID using the following format IF ( ID IS NOT BC Registry - * OR BC Service Card) THEN (example British Columbia Drivers License) { IF ( length < 7 values) - * THEN display the entire values ELSE [first value]***[Last three values] Example "1***756" END IF - * } ELSE IF ( BC Registry) Display the entire value. Example "FM232109" ELSE IF (BC Service Card) - * Display "BC Service card verified" END IF - *

- * Requirement - Birthday GIVEN Client detail summary edit mode WHEN in edit mode and role is - * CLIENT_VIEW THEN system will only show year of birth year. Example "1983" AND Column name is - * "Year of birth" as oppose to "Date of birth" - */ @Slf4j public class ForestClientObfuscate extends JsonSerializer { - private final List obfuscableFields = List.of("clientIdentification", "birthdate"); - + public static final String CLIENT_IDENTIFICATION = "clientIdentification"; + private final List obfuscableFields = List.of(CLIENT_IDENTIFICATION, "birthdate"); + + /** + * Serializes the given value, obfuscating certain fields based on user roles. + * + * @param value The value to serialize. + * @param gen The JSON generator used to write the JSON output. + * @param provider The serializer provider. + * @throws IOException If an I/O error occurs. + */ @SneakyThrows @Override public void serialize( @@ -40,6 +37,7 @@ public void serialize( if (value == null) { gen.writeNull(); + return; } gen.writeStartObject(); @@ -78,6 +76,14 @@ public void serialize( gen.writeEndObject(); } + /** + * Obfuscates the given property value based on the property name and type. + * + * @param propName The name of the property. + * @param propType The type of the property. + * @param value The value to obfuscate. + * @return The obfuscated value as a string. + */ private String obfuscate(String propName, String propType, Object value) { Set roles = toRoles(MDC.get(ApplicationConstant.MDC_USERROLES)); @@ -91,15 +97,15 @@ private String obfuscate(String propName, String propType, Object value) { } // BC Services card uses a UUID, so we just say it is verified - if ("clientIdentification".equals(propName) && "BCSC".equals(propType)) { + if (CLIENT_IDENTIFICATION.equals(propName) && "BCSC".equals(propType)) { return "BC Service card verified"; } - if ("clientIdentification".equals(propName) && "BCRE".equals(propType)) { + if (CLIENT_IDENTIFICATION.equals(propName) && "BCRE".equals(propType)) { return value.toString(); } - if ("clientIdentification".equals(propName)) { + if (CLIENT_IDENTIFICATION.equals(propName)) { return obfuscateClientIdentification(value.toString()); } @@ -110,10 +116,22 @@ private String obfuscate(String propName, String propType, Object value) { return value.toString(); } + /** + * Obfuscates the birthdate by masking the day and month. + * + * @param value The birthdate to obfuscate. + * @return The obfuscated birthdate as a string. + */ private String obfuscateBirthdate(LocalDate value) { return String.format("%d-**-**", value.getYear()); } + /** + * Obfuscates the client identification by masking the middle characters. + * + * @param value The client identification to obfuscate. + * @return The obfuscated client identification as a string. + */ private String obfuscateClientIdentification(String value) { if (value.length() < 7) { return value; @@ -122,6 +140,12 @@ private String obfuscateClientIdentification(String value) { return value.charAt(0) + "***" + value.substring(value.length() - 3); } + /** + * Converts a comma-separated string of roles into a set of roles. + * + * @param roleCsv The comma-separated string of roles. + * @return A set of roles. + */ private Set toRoles(String roleCsv) { if (StringUtils.isNotBlank(roleCsv)) { return Set.of(roleCsv.split(",")); @@ -129,4 +153,4 @@ private Set toRoles(String roleCsv) { return Set.of(); } -} +} \ No newline at end of file diff --git a/backend/src/main/java/ca/bc/gov/app/dto/legacy/ForestClientDetailsDto.java b/backend/src/main/java/ca/bc/gov/app/dto/legacy/ForestClientDetailsDto.java index 7077f27654..061c0e9ccf 100644 --- a/backend/src/main/java/ca/bc/gov/app/dto/legacy/ForestClientDetailsDto.java +++ b/backend/src/main/java/ca/bc/gov/app/dto/legacy/ForestClientDetailsDto.java @@ -4,6 +4,33 @@ import java.util.List; import lombok.With; +/** + * A Data Transfer Object (DTO) representing the details of a forest client. + * + * @param clientNumber The unique number identifying the client. + * @param clientName The name of the client. + * @param legalFirstName The legal first name of the client. + * @param legalMiddleName The legal middle name of the client. + * @param clientStatusCode The status code of the client. + * @param clientStatusDesc The description of the client's status. + * @param clientTypeCode The type code of the client. + * @param clientTypeDesc The description of the client's type. + * @param clientIdTypeCode The identification type code of the client. + * @param clientIdTypeDesc The description of the client's identification type. + * @param clientIdentification The identification of the client. + * @param registryCompanyTypeCode The registry company type code. + * @param corpRegnNmbr The corporate registration number. + * @param clientAcronym The acronym of the client. + * @param wcbFirmNumber The WCB (Workers' Compensation Board) firm number. + * @param clientComment Any comments about the client. + * @param clientCommentUpdateDate The date when the client comment was last updated. + * @param clientCommentUpdateUser The user who last updated the client comment. + * @param goodStandingInd Indicator of whether the client is in good standing. + * @param birthdate The birthdate of the client. + * @param addresses The list of addresses associated with the client. + * @param contacts The list of contacts associated with the client. + * @param doingBusinessAs The list of "doing business as" names associated with the client. + */ @With public record ForestClientDetailsDto( String clientNumber, @@ -32,4 +59,4 @@ public record ForestClientDetailsDto( List doingBusinessAs ) { -} +} \ No newline at end of file diff --git a/backend/src/main/java/ca/bc/gov/app/service/bcregistry/BcRegistryService.java b/backend/src/main/java/ca/bc/gov/app/service/bcregistry/BcRegistryService.java index a5afa22c71..ccc6bdd958 100644 --- a/backend/src/main/java/ca/bc/gov/app/service/bcregistry/BcRegistryService.java +++ b/backend/src/main/java/ca/bc/gov/app/service/bcregistry/BcRegistryService.java @@ -50,6 +50,7 @@ @Observed public class BcRegistryService { + public static final String REQUEST_BCREGISTRY = "request.bcregistry"; private final WebClient bcRegistryApi; private final ObservationRegistry registry; @@ -79,7 +80,8 @@ public Flux searchByFacets(String name, Str .uri("/registry-search/api/v2/search/businesses") .body(BodyInserters.fromValue( new BcRegistryFacetRequestBodyDto( - new BcRegistryFacetRequestQueryDto(Objects.toString(name, identifier), name, identifier), + new BcRegistryFacetRequestQueryDto(Objects.toString(name, identifier), name, + identifier), Map.of("status", List.of("ACTIVE")), 100, 0 @@ -112,16 +114,16 @@ public Flux searchByFacets(String name, Str ) ) .bodyToMono(BcRegistryFacetResponseDto.class) - .name("request.bcregistry") + .name(REQUEST_BCREGISTRY) .tag("kind", "facet") .tap(Micrometer.observation(registry)) .map(BcRegistryFacetResponseDto::searchResults) .flatMapIterable(BcRegistryFacetSearchResultsDto::results) .filter(entry -> entry.status().equalsIgnoreCase("active")) .doOnNext( - content -> log.info("Found entry on BC Registry [{}] {}", - content.identifier(), - content.name())); + content -> log.info("Found entry on BC Registry [{}] {}", + content.identifier(), + content.name())); } /** @@ -162,9 +164,9 @@ public Flux requestDocumentData(String value) { .bodyToMono(BcRegistryExceptionMessageDto.class) .map(BcRegistryExceptionMessageDto::rootCause) .doOnNext( - message -> log.error("Error while requesting data for {} -- {}", - value, - message)) + message -> log.error("Error while requesting data for {} -- {}", + value, + message)) .filter(message -> message.contains("not found")) .switchIfEmpty(Mono.error(new InvalidAccessTokenException())) .flatMap(message -> Mono.error(new NoClientDataFound(value))) @@ -183,7 +185,7 @@ public Flux requestDocumentData(String value) { ) ) .bodyToMono(BcRegistryDocumentRequestResponseDto.class) - .name("request.bcregistry") + .name(REQUEST_BCREGISTRY) .tag("kind", "docreq") .tap(Micrometer.observation(registry)) .flatMapIterable(BcRegistryDocumentRequestResponseDto::documents) @@ -220,7 +222,7 @@ private Mono getDocumentData(String identifier, String do exception -> Mono.error(new InvalidAccessTokenException()) ) .bodyToMono(BcRegistryDocumentDto.class) - .name("request.bcregistry") + .name(REQUEST_BCREGISTRY) .tag("kind", "docget") .tap(Micrometer.observation(registry)) .doOnNext( diff --git a/backend/src/main/java/ca/bc/gov/app/service/client/ClientLegacyService.java b/backend/src/main/java/ca/bc/gov/app/service/client/ClientLegacyService.java index 872deba389..a5ae0522f7 100644 --- a/backend/src/main/java/ca/bc/gov/app/service/client/ClientLegacyService.java +++ b/backend/src/main/java/ca/bc/gov/app/service/client/ClientLegacyService.java @@ -22,14 +22,14 @@ import reactor.core.publisher.Mono; /** - * This class is responsible for interacting with the legacy API to fetch client data. - * It uses the WebClient to send HTTP requests to the legacy API and converts the responses - * into Flux of ForestClientDto objects. It provides several methods to search for clients - * in the legacy system using different search criteria. - * - *

It is annotated with @Slf4j for logging, @Service to indicate that it's a + * This class is responsible for interacting with the legacy API to fetch client data. It uses the + * WebClient to send HTTP requests to the legacy API and converts the responses into Flux of + * ForestClientDto objects. It provides several methods to search for clients in the legacy system + * using different search criteria. + * + *

It is annotated with @Slf4j for logging, @Service to indicate that it's a * Spring service bean, and @Observed for metrics. - * + * *

Each method logs the search parameters and the results for debugging purposes. */ @Slf4j @@ -92,8 +92,8 @@ public Flux searchLegacy( /** * Searches for client details by client number using the legacy API. * - *

This method communicates with the legacy API to retrieve client information based on the - * provided client number. Optionally, a list of groups can be specified to refine the search + *

This method communicates with the legacy API to retrieve client information based on the + * provided client number. Optionally, a list of groups can be specified to refine the search * criteria. If a matching record is found, it is returned as a {@link ForestClientDetailsDto}. * * @param clientNumber the client number to search for @@ -113,7 +113,7 @@ public Mono searchByClientNumber(String clientNumber) { dto.clientNumber()) ); } - + /** * This method is used to search for a client in the legacy system using the client's ID and last * name. @@ -195,7 +195,7 @@ public Flux searchIndividual( // Convert the response to a Flux of ForestClientDto objects .exchangeToFlux(response -> response.bodyToFlux(ForestClientDto.class)) // Log the results for debugging purposes - .doOnNext(dto -> + .doOnNext(dto -> log.info( "Found data for first {} and last name {} in legacy with client number {}", firstName, lastName, dto.clientNumber() @@ -279,11 +279,11 @@ public Flux searchGeneric( if ( StringUtils.isBlank(searchType) - || parameters == null - || parameters.isEmpty() - || parameters.values().stream().anyMatch(CollectionUtils::isEmpty) - || parameters.values().stream().flatMap(List::stream).anyMatch(StringUtils::isBlank) - || parameters.keySet().stream().anyMatch(StringUtils::isBlank) + || parameters == null + || parameters.isEmpty() + || parameters.values().stream().anyMatch(CollectionUtils::isEmpty) + || parameters.values().stream().flatMap(List::stream).anyMatch(StringUtils::isBlank) + || parameters.keySet().stream().anyMatch(StringUtils::isBlank) ) { return Flux.empty(); } @@ -312,6 +312,12 @@ public Flux searchGeneric( } + /** + * Searches for clients in the legacy system based on the provided address details. + * + * @param dto The address search criteria. + * @return A Flux of ForestClientDto objects that match the search criteria. + */ public Flux searchLocation(AddressSearchDto dto) { return legacyApi @@ -325,6 +331,12 @@ public Flux searchLocation(AddressSearchDto dto) { ); } + /** + * Searches for clients in the legacy system based on the provided contact details. + * + * @param dto The contact search criteria. + * @return A Flux of ForestClientDto objects that match the search criteria. + */ public Flux searchContact(ContactSearchDto dto) { return legacyApi @@ -337,19 +349,28 @@ public Flux searchContact(ContactSearchDto dto) { client.clientNumber()) ); } - + + /** + * Searches for clients in the legacy system based on the provided keyword, page, and size. + * + * @param page The page number to retrieve. + * @param size The number of records per page. + * @param keyword The keyword to search for. + * @return A Flux of pairs containing ClientListDto objects and the total count of matching + * records. + */ public Flux> search(int page, int size, String keyword) { log.info( - "Searching clients by keyword {} with page {} and size {}", - keyword, - page, + "Searching clients by keyword {} with page {} and size {}", + keyword, + page, size ); return legacyApi .get() - .uri(builder -> - builder + .uri(builder -> + builder .path("/api/search") .queryParam("page", page) .queryParam("size", size) From bbbf47e9ae94653f2955f449ed5129a6aeb64168 Mon Sep 17 00:00:00 2001 From: Paulo Gomes da Cruz Junior Date: Wed, 18 Dec 2024 14:08:43 -0800 Subject: [PATCH 21/22] chore: fixing discrepancies --- .../gov/app/service/client/ClientService.java | 86 ++++++++++--------- .../ca/bc/gov/app/util/JwtPrincipalUtil.java | 73 +++++++++------- .../client/ClientServiceIntegrationTest.java | 1 - 3 files changed, 84 insertions(+), 76 deletions(-) diff --git a/backend/src/main/java/ca/bc/gov/app/service/client/ClientService.java b/backend/src/main/java/ca/bc/gov/app/service/client/ClientService.java index 56b8f8332a..dc7e7a2c3f 100644 --- a/backend/src/main/java/ca/bc/gov/app/service/client/ClientService.java +++ b/backend/src/main/java/ca/bc/gov/app/service/client/ClientService.java @@ -133,59 +133,61 @@ public Mono getClientDetailsByIncorporationNumber( }) // If document type is SP and party contains only one entry that is not a person, fail - .filter(document -> provider.equalsIgnoreCase("idir") - || !("SP".equalsIgnoreCase(document.business().legalType()) - && document.parties().size() == 1 - && !document.parties().get(0).isPerson()) + .filter(document -> provider.equalsIgnoreCase("idir") + || !("SP".equalsIgnoreCase(document.business().legalType()) + && document.parties().size() == 1 + && !document.parties().get(0).isPerson()) ) .flatMap(buildDetails()) .switchIfEmpty(Mono.error(new UnableToProcessRequestException( "Unable to process request. This sole proprietor is not owned by a person" ))); } - + public Mono getClientDetailsByClientNumber(String clientNumber) { - return legacyService - .searchByClientNumber(clientNumber) - .flatMap(forestClientDetailsDto -> Mono - .just(forestClientDetailsDto) - .filter(dto ->(StringUtils.isNotBlank(dto.corpRegnNmbr()))) - .doOnNext(dto -> log.info("Retrieved corporation registration number: {}", forestClientDetailsDto.corpRegnNmbr())) - .flatMap(dto -> - bcRegistryService - .requestDocumentData( dto.corpRegnNmbr()) - .next() - ) - .flatMap(documentMono -> populateGoodStandingInd(forestClientDetailsDto, documentMono) ) - .onErrorContinue(NoClientDataFound.class, (ex, obj) -> - log.error("No data found on BC Registry for client number: {}", clientNumber) - ) - .switchIfEmpty( - Mono.just(forestClientDetailsDto) - .doOnNext(dto -> log.info("Corporation registration number not provided. Returning legacy details.")) - ) - ); + return legacyService + .searchByClientNumber(clientNumber) + .flatMap(forestClientDetailsDto -> Mono + .just(forestClientDetailsDto) + .filter(dto -> (StringUtils.isNotBlank(dto.corpRegnNmbr()))) + .doOnNext(dto -> log.info("Retrieved corporation registration number: {}", + forestClientDetailsDto.corpRegnNmbr())) + .flatMap(dto -> + bcRegistryService + .requestDocumentData(dto.corpRegnNmbr()) + .next() + ) + .flatMap(documentMono -> populateGoodStandingInd(forestClientDetailsDto, documentMono)) + .onErrorContinue(NoClientDataFound.class, (ex, obj) -> + log.error("No data found on BC Registry for client number: {}", clientNumber) + ) + .switchIfEmpty( + Mono.just(forestClientDetailsDto) + .doOnNext(dto -> log.info( + "Corporation registration number not provided. Returning legacy details.")) + ) + ); } private Mono populateGoodStandingInd( ForestClientDetailsDto forestClientDetailsDto, BcRegistryDocumentDto document ) { - Boolean goodStandingInd = document.business().goodStanding(); - String goodStanding = BooleanUtils.toString( - goodStandingInd, - "Y", - "N", - StringUtils.EMPTY - ); + Boolean goodStandingInd = document.business().goodStanding(); + String goodStanding = BooleanUtils.toString( + goodStandingInd, + "Y", + "N", + StringUtils.EMPTY + ); - log.info("Setting goodStandingInd for client: {} to {}", - forestClientDetailsDto.clientNumber(), goodStanding); + log.info("Setting goodStandingInd for client: {} to {}", + forestClientDetailsDto.clientNumber(), goodStanding); - ForestClientDetailsDto updatedDetails = - forestClientDetailsDto.withGoodStandingInd(goodStanding); + ForestClientDetailsDto updatedDetails = + forestClientDetailsDto.withGoodStandingInd(goodStanding); - return Mono.just(updatedDetails); + return Mono.just(updatedDetails); } /** @@ -402,13 +404,13 @@ private Predicate isMatchWith(BcRegistryDocumentDto document) { return legacy -> StringUtils.equals( StringUtils.defaultString(legacy.registryCompanyTypeCode()) + - StringUtils.defaultString(legacy.corpRegnNmbr()), + StringUtils.defaultString(legacy.corpRegnNmbr()), document.business().identifier() ) && - StringUtils.equals( - document.business().legalName(), - legacy.legalName() - ); + StringUtils.equals( + document.business().legalName(), + legacy.legalName() + ); } private Function> triggerEmailDuplicatedClient( diff --git a/backend/src/main/java/ca/bc/gov/app/util/JwtPrincipalUtil.java b/backend/src/main/java/ca/bc/gov/app/util/JwtPrincipalUtil.java index 8f73d59f3f..63e1bbba6f 100644 --- a/backend/src/main/java/ca/bc/gov/app/util/JwtPrincipalUtil.java +++ b/backend/src/main/java/ca/bc/gov/app/util/JwtPrincipalUtil.java @@ -21,6 +21,8 @@ @NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) public class JwtPrincipalUtil { + public static final String LAST_NAME = "lastName"; + /** * Retrieves the provider of the JWT token from the given JwtAuthenticationToken principal. The * provider is extracted from the token attributes under the key "custom:idp_name". If the @@ -29,7 +31,7 @@ public class JwtPrincipalUtil { * * @param principal JwtAuthenticationToken object from which the provider is to be extracted. * @return The provider of the JWT token in uppercase, or an empty string if the provider is - * blank. + * blank. */ public static String getProvider(JwtAuthenticationToken principal) { return getProviderValue(principal.getTokenAttributes()); @@ -43,7 +45,7 @@ public static String getProvider(JwtAuthenticationToken principal) { * * @param principal Jwt object from which the provider is to be extracted. * @return The provider of the JWT token in uppercase, or an empty string if the provider is - * blank. + * blank. */ public static String getProvider(Jwt principal) { return getProviderValue(principal.getClaims()); @@ -58,7 +60,7 @@ public static String getProvider(Jwt principal) { * * @param principal JwtAuthenticationToken object from which the user ID is to be extracted. * @return The user ID prefixed with the provider in uppercase and a backslash, or an empty string - * if the user ID is blank. + * if the user ID is blank. */ public static String getUserId(JwtAuthenticationToken principal) { return getUserIdValue(principal.getTokenAttributes()); @@ -72,7 +74,7 @@ public static String getUserId(JwtAuthenticationToken principal) { * * @param principal Jwt object from which the user ID is to be extracted. * @return The user ID prefixed with the provider in uppercase and a backslash, or an empty string - * if the user ID is blank. + * if the user ID is blank. */ public static String getUserId(Jwt principal) { return getUserIdValue(principal.getClaims()); @@ -174,7 +176,7 @@ public static String getName(JwtAuthenticationToken principal) { * * @param principal Jwt object from which the display name is to be extracted. * @return The display name, or the concatenated first and last names, or an empty string if both - * the display name and the first and last names are blank. + * the display name and the first and last names are blank. */ public static String getName(Jwt principal) { return getNameValue(principal.getClaims()); @@ -205,14 +207,17 @@ public static String getLastName(Jwt principal) { } /** - * Retrieves a list of groups from the given JwtPrincipal. - * This method extracts the token attributes from the provided {@link JwtAuthenticationToken}, then looks for the key "cognito:groups" - * in the token attributes. If the value associated with this key is a {@link List}, the method filters the elements to only - * include non-null values of type {@link String}. The resulting list of strings is returned. + * Retrieves a list of groups from the given JwtPrincipal. This method extracts the token + * attributes from the provided {@link JwtAuthenticationToken}, then looks for the key + * "cognito:groups" in the token attributes. If the value associated with this key is a + * {@link List}, the method filters the elements to only include non-null values of type + * {@link String}. The resulting list of strings is returned. * - * @param jwtPrincipal The {@link JwtAuthenticationToken} containing the token attributes. It must have the "cognito:groups" key. - * If the key does not exist or the value is not a list of strings, an empty list is returned. - * @return A list of group names, or an empty list if the key is missing or the value is not a list of strings. + * @param jwtPrincipal The {@link JwtAuthenticationToken} containing the token attributes. It must + * have the "cognito:groups" key. If the key does not exist or the value is + * not a list of strings, an empty list is returned. + * @return A list of group names, or an empty list if the key is missing or the value is not a + * list of strings. */ public static Set getGroups(JwtAuthenticationToken jwtPrincipal) { if (jwtPrincipal == null || jwtPrincipal.getTokenAttributes() == null) { @@ -222,14 +227,17 @@ public static Set getGroups(JwtAuthenticationToken jwtPrincipal) { } /** - * Retrieves a list of groups from the given JwtPrincipal. - * This method extracts the token attributes from the provided {@link Jwt}, then looks for the key "cognito:groups" - * in the token attributes. If the value associated with this key is a {@link List}, the method filters the elements to only - * include non-null values of type {@link String}. The resulting list of strings is returned. + * Retrieves a list of groups from the given JwtPrincipal. This method extracts the token + * attributes from the provided {@link Jwt}, then looks for the key "cognito:groups" in the token + * attributes. If the value associated with this key is a {@link List}, the method filters the + * elements to only include non-null values of type {@link String}. The resulting list of strings + * is returned. * - * @param jwtPrincipal The {@link Jwt} containing the token attributes. It must have the "cognito:groups" key. - * If the key does not exist or the value is not a list of strings, an empty list is returned. - * @return A list of group names, or an empty list if the key is missing or the value is not a list of strings. + * @param jwtPrincipal The {@link Jwt} containing the token attributes. It must have the + * "cognito:groups" key. If the key does not exist or the value is not a list + * of strings, an empty list is returned. + * @return A list of group names, or an empty list if the key is missing or the value is not a + * list of strings. */ public static Set getGroups(Jwt jwtPrincipal) { if (jwtPrincipal == null || jwtPrincipal.getClaims() == null) { @@ -238,7 +246,7 @@ public static Set getGroups(Jwt jwtPrincipal) { return getClaimGroups(jwtPrincipal.getClaims()); } - private static Set getClaimGroups(Map tokenAttributes) { + private static Set getClaimGroups(Map tokenAttributes) { Object groups = tokenAttributes.get("cognito:groups"); if (groups instanceof List) { @@ -259,7 +267,7 @@ private static Set getClaimGroups(Map tokenAttributes) { * @param claims The map containing the JWT claims. * @param claimName The name of the claim to retrieve. * @return The value of the specified claim as a String, or an empty string if the claim is not - * present. + * present. */ private static String getClaimValue(Map claims, String claimName) { return claims @@ -275,7 +283,7 @@ private static String getClaimValue(Map claims, String claimName * * @param claims The map containing the JWT claims. * @return The provider's name in uppercase or "BCSC" if it starts with "ca.bc.gov.flnr.fam.", or - * an empty string if the provider is not specified. + * an empty string if the provider is not specified. */ private static String getProviderValue(Map claims) { String provider = getClaimValue(claims, "custom:idp_name"); @@ -309,7 +317,7 @@ private static String getBusinessNameValue(Map claims) { * * @param claims The map containing the JWT claims. * @return The constructed user ID in the format "Provider\Username" or "Provider\UserID", or an - * empty string if neither the username nor the user ID is present in the claims. + * empty string if neither the username nor the user ID is present in the claims. */ private static String getUserIdValue(Map claims) { return @@ -359,7 +367,7 @@ private static String getEmailValue(Map claims) { * * @param claims The map containing the JWT claims. * @return The display name value as a String, or an empty string if the "custom:idp_display_name" - * claim is not present. + * claim is not present. */ private static String getDisplayNameValue(Map claims) { return getClaimValue(claims, "custom:idp_display_name"); @@ -374,10 +382,9 @@ private static String getDisplayNameValue(Map claims) { * * @param claims The map containing the JWT claims from which the name information is to be * extracted. - * @return A map with keys "businessName", "firstName", "lastName", and "fullName", - * containing the extracted and/or computed name information. - * If specific name components are not found, their values in the map - * will be empty strings. + * @return A map with keys "businessName", "firstName", "lastName", and "fullName", containing the + * extracted and/or computed name information. If specific name components are not found, their + * values in the map will be empty strings. */ private static Map processName(Map claims) { Map additionalInfo = new HashMap<>(); @@ -392,17 +399,17 @@ private static Map processName(Map claims) { // Determine if special handling for names is required boolean useDisplayName = "bceidbusiness".equals(getProviderValue(claims)) || (firstName.isEmpty() - && lastName.isEmpty()); + && lastName.isEmpty()); if (useDisplayName) { Map names = ClientMapper.parseName(getDisplayNameValue(claims), getProviderValue(claims)); firstName = names.get("firstName"); - lastName = names.get("lastName"); + lastName = names.get(LAST_NAME); } // Put extracted or computed first and last names into the map additionalInfo.put("firstName", firstName.trim()); - additionalInfo.put("lastName", lastName.trim()); + additionalInfo.put(LAST_NAME, lastName.trim()); additionalInfo.put("fullName", String.join(" ", firstName, lastName).trim()); return additionalInfo; @@ -417,7 +424,7 @@ private static Map processName(Map claims) { * @return The last name extracted from the JWT claims, or an empty string if not specified. */ private static String getLastNameValue(Map claims) { - return processName(claims).get("lastName"); + return processName(claims).get(LAST_NAME); } /** @@ -428,7 +435,7 @@ private static String getLastNameValue(Map claims) { * * @param claims The map containing the JWT claims. * @return The full name (concatenation of first and last names) extracted from the JWT claims, or - * an empty string if not specified. + * an empty string if not specified. */ private static String getNameValue(Map claims) { return processName(claims).get("fullName"); diff --git a/backend/src/test/java/ca/bc/gov/app/service/client/ClientServiceIntegrationTest.java b/backend/src/test/java/ca/bc/gov/app/service/client/ClientServiceIntegrationTest.java index b937e5a945..8a97011501 100644 --- a/backend/src/test/java/ca/bc/gov/app/service/client/ClientServiceIntegrationTest.java +++ b/backend/src/test/java/ca/bc/gov/app/service/client/ClientServiceIntegrationTest.java @@ -41,7 +41,6 @@ class ClientServiceIntegrationTest extends AbstractTestContainerIntegrationTest void testGetClientDetailsWithGoodStandingIndicator() { String clientNumber = "123456"; String corpRegnNmbr = "9607514"; - List groups = List.of("CLIENT_ADMIN"); ForestClientDetailsDto initialDto = new ForestClientDetailsDto( clientNumber, From 43ee1d6e7b2085ea890f65bb5cb76347b979641e Mon Sep 17 00:00:00 2001 From: Paulo Gomes da Cruz Junior Date: Thu, 19 Dec 2024 05:59:20 -0800 Subject: [PATCH 22/22] test: adding test --- .../app/converters/ForestClientObfuscate.java | 4 +- .../converters/ForestClientObfuscateTest.java | 102 ++++++++++++++++++ 2 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 backend/src/test/java/ca/bc/gov/app/converters/ForestClientObfuscateTest.java diff --git a/backend/src/main/java/ca/bc/gov/app/converters/ForestClientObfuscate.java b/backend/src/main/java/ca/bc/gov/app/converters/ForestClientObfuscate.java index 29b6bc7348..86fe7e0672 100644 --- a/backend/src/main/java/ca/bc/gov/app/converters/ForestClientObfuscate.java +++ b/backend/src/main/java/ca/bc/gov/app/converters/ForestClientObfuscate.java @@ -17,7 +17,7 @@ public class ForestClientObfuscate extends JsonSerializer { public static final String CLIENT_IDENTIFICATION = "clientIdentification"; - private final List obfuscableFields = List.of(CLIENT_IDENTIFICATION, "birthdate"); + private final List obfuscateFields = List.of(CLIENT_IDENTIFICATION, "birthdate"); /** * Serializes the given value, obfuscating certain fields based on user roles. @@ -63,7 +63,7 @@ public void serialize( clientIdTypeCode = rawValue.toString(); } - if (obfuscableFields.contains(propName)) { + if (obfuscateFields.contains(propName)) { gen.writeString(obfuscate(propName, clientIdTypeCode, rawValue)); continue; } diff --git a/backend/src/test/java/ca/bc/gov/app/converters/ForestClientObfuscateTest.java b/backend/src/test/java/ca/bc/gov/app/converters/ForestClientObfuscateTest.java new file mode 100644 index 0000000000..7a649270fc --- /dev/null +++ b/backend/src/test/java/ca/bc/gov/app/converters/ForestClientObfuscateTest.java @@ -0,0 +1,102 @@ +package ca.bc.gov.app.converters; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import ca.bc.gov.app.ApplicationConstant; +import ca.bc.gov.app.dto.legacy.ForestClientDetailsDto; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import java.time.LocalDate; +import java.util.List; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.slf4j.MDC; + +class ForestClientObfuscateTest { + + private static final ObjectMapper mapper = new ObjectMapper(); + + @BeforeAll + public static void setUp() { + SimpleModule module = new SimpleModule(); + module.setSerializerModifier(new ForestClientDetailsSerializerModifier()); + mapper.registerModule(new JavaTimeModule()); + mapper.registerModule(module); + + } + + @Test + void shouldObfuscateIdentification() throws Exception { + MDC.put(ApplicationConstant.MDC_USERROLES, ApplicationConstant.ROLE_VIEWER); + String json = mapper.writeValueAsString(getDto()); + assertThat(json) + .contains("\"clientIdentification\":\"1***678\"") + .contains("\"birthdate\":\"1070-**-**\""); + } + + @Test + void shouldObfuscateBCSC() throws Exception { + MDC.put(ApplicationConstant.MDC_USERROLES, ApplicationConstant.ROLE_VIEWER); + String json = mapper.writeValueAsString(getDto().withClientIdTypeCode("BCSC")); + assertThat(json) + .contains("\"clientIdentification\":\"BC Service card verified\"") + .contains("\"birthdate\":\"1070-**-**\""); + } + + @Test + void shouldNotObfuscateSmallIds() throws Exception { + MDC.put(ApplicationConstant.MDC_USERROLES, ApplicationConstant.ROLE_VIEWER); + String json = mapper.writeValueAsString(getDto().withClientIdentification("1234")); + assertThat(json) + .contains("\"clientIdentification\":\"1234\"") + .contains("\"birthdate\":\"1070-**-**\""); + } + + @Test + void shouldNotObfuscateForNonViewers() throws Exception { + MDC.put(ApplicationConstant.MDC_USERROLES, ApplicationConstant.ROLE_EDITOR); + String json = mapper.writeValueAsString(getDto()); + assertThat(json) + .contains("\"clientIdentification\":\"12345678\"") + .contains("\"birthdate\":\"1070-12-13\""); + } + + @Test + void shouldNotObfuscateEvenIfHasViewer() throws Exception { + MDC.put(ApplicationConstant.MDC_USERROLES, String.format("%s,%s", ApplicationConstant.ROLE_EDITOR, ApplicationConstant.ROLE_VIEWER)); + String json = mapper.writeValueAsString(getDto()); + assertThat(json) + .contains("\"clientIdentification\":\"12345678\"") + .contains("\"birthdate\":\"1070-12-13\""); + } + + private ForestClientDetailsDto getDto() { + return new ForestClientDetailsDto( + "123456789", + "Wick", + "Johnathan", + null, + "ACT", + "Active", + "I", + "Individual", + "BCDL", + "BC Drivers License", + "12345678", + null, + null, + null, + null, + null, + null, + null, + null, + LocalDate.of(1070, 12, 13), + List.of(), + List.of(), + List.of() + ); + } + +} \ No newline at end of file