Skip to content

Commit

Permalink
feat(FSADT1-1631): updating details api for legacy clients (#1356)
Browse files Browse the repository at this point in the history
* feat(FSADT1-1631): removing unused code

* chore: updating legacy api

* feat(FSADT1-1631): added context propagator for roles

- Reused the MDC for role context propagator.
- Refactored the UserID filter
- Extracted the context propagator filter

* chore: small update on the DTO

* chore: added the role mdc constant

* feat(FSADT1-1631): updated principal util roles to set

- Changed to SET from LIST to account for unique values

* feat(FSADT1-1631): added client details obfuscator

- This will obfuscate values based on roles and fields

* feat(FSADT1-1631): added location and contact load

* test(FSADT1-1631): added tests

* chore: removed print

* Doing code reviews

* Doing code reviews

* chore: fixing discrepancies

* chore: fixing sonar issues

* chore: fixing sonar issues

* chore: fixing sonar issues

* chore: fixing sonar issues

* chore: fixing sonar issues

* chore: fixing sonar issues

* chore: fixing discrepancies

* chore: fixing discrepancies

* test: adding test

---------

Co-authored-by: Maria Martinez <[email protected]>
  • Loading branch information
paulushcgcj and mamartinezmejia authored Dec 19, 2024
1 parent d162b23 commit 803f583
Show file tree
Hide file tree
Showing 22 changed files with 1,105 additions and 372 deletions.
48 changes: 39 additions & 9 deletions backend/src/main/java/ca/bc/gov/app/ApplicationConstant.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 = """
Expand All @@ -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
Expand All @@ -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""";

Expand All @@ -108,9 +124,23 @@ 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 = "<Filter><Or><PropertyIsLike wildCard=\"*\" singleChar=\".\" escape=\"!\"><PropertyName>%s</PropertyName><Literal>*%s*</Literal></PropertyIsLike><PropertyIsLike wildCard=\"*\" singleChar=\".\" escape=\"!\"><PropertyName>%s</PropertyName><Literal>*%s*</Literal></PropertyIsLike><PropertyIsLike wildCard=\"*\" singleChar=\".\" escape=\"!\"><PropertyName>%s</PropertyName><Literal>*%s*</Literal></PropertyIsLike><PropertyIsLike wildCard=\"*\" singleChar=\".\" escape=\"!\"><PropertyName>%s</PropertyName><Literal>*%s*</Literal></PropertyIsLike></Or></Filter>";
public static final String OPENDATA_FILTER = "<Filter><Or><PropertyIsLike wildCard=\"*\" "
+ "singleChar=\".\" escape=\"!\">"
+ "<PropertyName>%s</PropertyName>"
+ "<Literal>*%s*</Literal></PropertyIsLike>"
+ "<PropertyIsLike wildCard=\"*\" singleChar=\".\" "
+ "escape=\"!\"><PropertyName>%s</PropertyName>"
+ "<Literal>*%s*</Literal></PropertyIsLike>"
+ "<PropertyIsLike wildCard=\"*\" singleChar=\".\" "
+ "escape=\"!\"><PropertyName>%s</PropertyName>"
+ "<Literal>*%s*</Literal></PropertyIsLike>"
+ "<PropertyIsLike wildCard=\"*\" singleChar=\".\" "
+ "escape=\"!\"><PropertyName>%s</PropertyName>"
+ "<Literal>*%s*</Literal></PropertyIsLike>"
+ "</Or></Filter>";

public static final String MDC_USERID = "X-USER";

public static final String MDC_USERROLES = "X-Role";
}

Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -245,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,
Expand Down Expand Up @@ -272,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,
Expand Down Expand Up @@ -303,9 +325,36 @@ 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) {
return builder.build();

ObjectMapper mapper = builder.build();
mapper.registerModule(new JavaTimeModule());
mapper.registerModule(forestClientDetailsDtoModule());

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();

// Register the serializer modifier
module.setSerializerModifier(new ForestClientDetailsSerializerModifier());
return module;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand All @@ -64,44 +64,25 @@ public Mono<ClientDetailsDto> getClientDetailsByIncorporationNumber(
JwtPrincipalUtil.getProvider(principal)
);
}

/**
* Handles HTTP GET requests to retrieve client details based on the provided client number.
*
* <p>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.</p>
*
* @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<ForestClientDetailsDto> 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));
@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.
*
* <p>This endpoint allows searching for clients by a keyword. The results are paginated, and the
* <p>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
*/
Expand All @@ -128,15 +109,15 @@ public Flux<ClientListDto> 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.
*
Expand All @@ -154,18 +135,18 @@ public Flux<ClientLookUpDto> findByClientName(@PathVariable String name) {
/**
* Finds a client based on their registration number.
*
* <p>This endpoint retrieves client information by searching for a registration number.
* <p>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<ClientLookUpDto> findByRegistrationNumber(
@PathVariable String registrationNumber) {
log.info("Requesting a client with registration number {} from the client service.",
registrationNumber);
registrationNumber);
return clientService
.findByClientNameOrIncorporation(registrationNumber)
.next()
Expand All @@ -175,10 +156,10 @@ public Mono<ClientLookUpDto> findByRegistrationNumber(
/**
* Searches for an individual client by user ID and last name.
*
* <p>This endpoint fetches an individual client using their user ID and last name.
* <p>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
*/
Expand All @@ -187,9 +168,9 @@ public Mono<Void> 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);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
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;

/**
* 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();

protected abstract Function<Authentication, String> 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 {@link Mono} of {@link Void} that indicates when request handling is complete.
*/
@Override
public Mono<Void> 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())
);
}

}
Loading

0 comments on commit 803f583

Please sign in to comment.