Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(FSADT1-1631): updating details api for legacy clients #1356

Merged
merged 28 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
7a31863
feat(FSADT1-1631): removing unused code
paulushcgcj Dec 9, 2024
d277516
chore: updating legacy api
paulushcgcj Dec 9, 2024
d8cbb49
Merge branch 'main' into feat/FSADT1-1631
paulushcgcj Dec 9, 2024
c452c61
Merge branch 'main' into feat/FSADT1-1631
paulushcgcj Dec 17, 2024
0bc8600
feat(FSADT1-1631): added context propagator for roles
paulushcgcj Dec 17, 2024
8045989
chore: small update on the DTO
paulushcgcj Dec 17, 2024
b146cf2
chore: added the role mdc constant
paulushcgcj Dec 17, 2024
bd23321
feat(FSADT1-1631): updated principal util roles to set
paulushcgcj Dec 17, 2024
eb7ba7c
feat(FSADT1-1631): added client details obfuscator
paulushcgcj Dec 17, 2024
318db86
feat(FSADT1-1631): added location and contact load
paulushcgcj Dec 17, 2024
200e0f4
test(FSADT1-1631): added tests
paulushcgcj Dec 17, 2024
0c52c1b
chore: removed print
paulushcgcj Dec 17, 2024
bdd9bc2
Merge branch 'main' into feat/FSADT1-1631
paulushcgcj Dec 17, 2024
3bf5232
Merge branch 'main' into feat/FSADT1-1631
paulushcgcj Dec 18, 2024
baf3c7b
Merge branch 'main' into feat/FSADT1-1631
paulushcgcj Dec 18, 2024
77265e3
Merge branch 'main' into feat/FSADT1-1631
paulushcgcj Dec 18, 2024
fadce8f
Doing code reviews
mamartinezmejia Dec 18, 2024
656533e
Doing code reviews
mamartinezmejia Dec 18, 2024
983bf40
chore: fixing discrepancies
paulushcgcj Dec 18, 2024
6e2cf93
chore: fixing sonar issues
paulushcgcj Dec 18, 2024
0c50de1
chore: fixing sonar issues
paulushcgcj Dec 18, 2024
5da9dbe
chore: fixing sonar issues
paulushcgcj Dec 18, 2024
abf26b2
chore: fixing sonar issues
paulushcgcj Dec 18, 2024
9952ec3
chore: fixing sonar issues
paulushcgcj Dec 18, 2024
2705586
chore: fixing sonar issues
paulushcgcj Dec 18, 2024
6a6ae7b
chore: fixing discrepancies
paulushcgcj Dec 18, 2024
bbbf47e
chore: fixing discrepancies
paulushcgcj Dec 18, 2024
43ee1d6
test: adding test
paulushcgcj Dec 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}

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 @@ -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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -64,32 +64,11 @@ 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));
public Mono<ForestClientDetailsDto> getClientDetailsByClientNumber(@PathVariable String clientNumber) {
log.info("Requesting client details for client number {}", clientNumber);
return clientService.getClientDetailsByClientNumber(clientNumber);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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<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 Mono<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())
);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;


/**
Expand All @@ -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<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(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;
}

/**
Expand All @@ -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<Authentication, String> 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";
};
}

}
Original file line number Diff line number Diff line change
@@ -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<Authentication, String> getContextValueExtractor() {
return authentication -> {
Set<String> 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);
};
}

}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading
Loading