Skip to content

Commit

Permalink
feat: implement profile management on oracle (#1080)
Browse files Browse the repository at this point in the history
  • Loading branch information
Ricardo Campos authored and DerekRoberts committed May 14, 2024
1 parent bff6d66 commit 359f261
Show file tree
Hide file tree
Showing 19 changed files with 373 additions and 100 deletions.
1 change: 1 addition & 0 deletions oracle-api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@
<excludes>
<exclude>**/config/**</exclude>
<exclude>**/filter/**</exclude>
<exclude>**/interceptor/**</exclude>
<exclude>**/*$*Builder*</exclude>
<exclude>**/BackendStartApiApplication.*</exclude>
</excludes>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package ca.bc.gov.backendstartapi.config;

import ca.bc.gov.backendstartapi.interceptor.RoleAccessInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.lang.NonNull;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/** This class simply add the Crud Matrix interceptor in the request chain. */
@Configuration
public class RoleAccessInterceptorConfig implements WebMvcConfigurer {

@Override
public void addInterceptors(@NonNull InterceptorRegistry registry) {
registry.addInterceptor(new RoleAccessInterceptor());
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package ca.bc.gov.backendstartapi.config;

import java.util.ArrayList;
import ca.bc.gov.backendstartapi.security.JwtSecurityUtil;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand Down Expand Up @@ -70,17 +71,11 @@ private Converter<Jwt, AbstractAuthenticationToken> converter() {

private final Converter<Jwt, Collection<GrantedAuthority>> roleConverter =
jwt -> {
if (!jwt.getClaims().containsKey("client_roles")) {
if (!jwt.getClaims().containsKey("cognito:groups")) {
return List.of();
}
Object clientRolesObj = jwt.getClaims().get("client_roles");
final List<String> realmAccess = new ArrayList<>();
if (clientRolesObj instanceof List<?> list) {
for (Object item : list) {
realmAccess.add(String.valueOf(item));
}
}
return realmAccess.stream()
Set<String> rolesSet = JwtSecurityUtil.getUserRolesFromJwt(jwt);
return rolesSet.stream()
.map(roleName -> "ROLE_" + roleName)
.map(roleName -> (GrantedAuthority) new SimpleGrantedAuthority(roleName))
.toList();
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import ca.bc.gov.backendstartapi.config.SparLog;
import ca.bc.gov.backendstartapi.entity.FacilityTypes;
import ca.bc.gov.backendstartapi.repository.FacilityTypesRepository;
import ca.bc.gov.backendstartapi.security.RoleAccessConfig;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
Expand Down Expand Up @@ -53,6 +54,7 @@ public class FacilityTypesEndpoint {
description = "Access token is missing or invalid",
content = @Content(schema = @Schema(implementation = Void.class)))
})
@RoleAccessConfig({"SPAR_TSC_ADMIN", "SPAR_MINISTRY_ORCHARD", "SPAR_NONMINISTRY_ORCHARD"})
public List<FacilityTypes> getAllValidFacilityTypes() {
SparLog.info("Fetching all valid facility types");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import ca.bc.gov.backendstartapi.config.SparLog;
import ca.bc.gov.backendstartapi.entity.FundingSource;
import ca.bc.gov.backendstartapi.repository.FundingSourceRepository;
import ca.bc.gov.backendstartapi.security.RoleAccessConfig;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
Expand Down Expand Up @@ -51,9 +52,10 @@ public class FundingSourceEndpoint {
description = "Access token is missing or invalid",
content = @Content(schema = @Schema(implementation = Void.class)))
})
@RoleAccessConfig({"SPAR_TSC_ADMIN", "SPAR_MINISTRY_ORCHARD", "SPAR_NONMINISTRY_ORCHARD"})
public List<FundingSource> getAllValidFundingSources() {
SparLog.info("Fetching all valid funding sources");

List<FundingSource> resultList = fundingSourceRepository.findAllValid();
SparLog.info("{} valid funding sources found.", resultList.size());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import ca.bc.gov.backendstartapi.dto.SameSpeciesTreeDto;
import ca.bc.gov.backendstartapi.dto.SeedPlanZoneDto;
import ca.bc.gov.backendstartapi.entity.Orchard;
import ca.bc.gov.backendstartapi.security.RoleAccessConfig;
import ca.bc.gov.backendstartapi.service.OrchardService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
Expand Down Expand Up @@ -64,6 +65,7 @@ public class OrchardEndpoint {
content = @Content(schema = @Schema(implementation = Void.class))),
@ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true)))
})
@RoleAccessConfig({"SPAR_TSC_ADMIN", "SPAR_MINISTRY_ORCHARD", "SPAR_NONMINISTRY_ORCHARD"})
public OrchardLotTypeDescriptionDto getOrchardById(
@PathVariable
@Parameter(name = "id", in = ParameterIn.PATH, description = "Identifier of the orchard.")
Expand Down Expand Up @@ -99,6 +101,7 @@ public OrchardLotTypeDescriptionDto getOrchardById(
content = @Content(schema = @Schema(implementation = Void.class))),
@ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true)))
})
@RoleAccessConfig({"SPAR_TSC_ADMIN", "SPAR_MINISTRY_ORCHARD", "SPAR_NONMINISTRY_ORCHARD"})
public OrchardParentTreeDto getParentTreeGeneticQualityData(
@PathVariable
@Parameter(
Expand Down Expand Up @@ -145,6 +148,7 @@ public OrchardParentTreeDto getParentTreeGeneticQualityData(
content = @Content(schema = @Schema(implementation = Void.class))),
@ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true)))
})
@RoleAccessConfig({"SPAR_TSC_ADMIN", "SPAR_MINISTRY_ORCHARD", "SPAR_NONMINISTRY_ORCHARD"})
public List<OrchardLotTypeDescriptionDto> getOrchardsByVegCode(
@PathVariable("vegCode") @Parameter(description = "The vegetation code of an orchard.")
String vegCode) {
Expand All @@ -169,6 +173,7 @@ public List<OrchardLotTypeDescriptionDto> getOrchardsByVegCode(
path = "/parent-trees/vegetation-codes/{vegCode}",
consumes = "application/json",
produces = "application/json")
@RoleAccessConfig({"SPAR_TSC_ADMIN", "SPAR_MINISTRY_ORCHARD", "SPAR_NONMINISTRY_ORCHARD"})
public ResponseEntity<List<SameSpeciesTreeDto>> findParentTreesWithVegCode(
@PathVariable("vegCode")
@Parameter(description = "The vegetation code of an orchard.")
Expand Down Expand Up @@ -208,6 +213,7 @@ public ResponseEntity<List<SameSpeciesTreeDto>> findParentTreesWithVegCode(
description = "Access token is missing or invalid",
content = @Content(schema = @Schema(implementation = Void.class)))
})
@RoleAccessConfig({"SPAR_TSC_ADMIN", "SPAR_MINISTRY_ORCHARD", "SPAR_NONMINISTRY_ORCHARD"})
public List<SeedPlanZoneDto> getSpzInformation(
@Parameter(description = "The SPU (Seed Planning Unit) ID list") @PathVariable("spuIds")
Integer[] spuIds) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import ca.bc.gov.backendstartapi.dto.GeospatialRequestDto;
import ca.bc.gov.backendstartapi.dto.GeospatialRespondDto;
import ca.bc.gov.backendstartapi.entity.ParentTreeEntity;
import ca.bc.gov.backendstartapi.security.RoleAccessConfig;
import ca.bc.gov.backendstartapi.service.ParentTreeService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
Expand Down Expand Up @@ -47,6 +48,7 @@ public class ParentTreeEndpoint {
description = "Access token is missing or invalid",
content = @Content(schema = @Schema(implementation = Void.class)))
})
@RoleAccessConfig({"SPAR_TSC_ADMIN", "SPAR_MINISTRY_ORCHARD", "SPAR_NONMINISTRY_ORCHARD"})
public List<GeospatialRespondDto> getPtGeoSpatialData(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "A list of Parent Tree id to fetch geospatial data.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import ca.bc.gov.backendstartapi.endpoint.parameters.PaginationParameters;
import ca.bc.gov.backendstartapi.entity.VegetationCode;
import ca.bc.gov.backendstartapi.repository.VegetationCodeRepository;
import ca.bc.gov.backendstartapi.security.RoleAccessConfig;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
Expand Down Expand Up @@ -59,6 +60,7 @@ public class VegetationCodeEndpoint {
@ApiResponse(responseCode = "200"),
@ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true)))
})
@RoleAccessConfig({"SPAR_TSC_ADMIN", "SPAR_MINISTRY_ORCHARD", "SPAR_NONMINISTRY_ORCHARD"})
public VegetationCode findByCode(
@PathVariable("code")
@Parameter(
Expand Down Expand Up @@ -101,6 +103,7 @@ Search for valid vegetation codes (ones which `effectiveDate` ≤ today < `expir
"An array with the vegetation codes found, ordered by their identifiers."),
@ApiResponse(responseCode = "400", content = @Content(schema = @Schema(hidden = true)))
})
@RoleAccessConfig({"SPAR_TSC_ADMIN", "SPAR_MINISTRY_ORCHARD", "SPAR_NONMINISTRY_ORCHARD"})
@PaginatedViaQuery
public List<VegetationCode> findEffectiveByCodeOrDescription(
@RequestParam(name = "search", defaultValue = "")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package ca.bc.gov.backendstartapi.interceptor;

import ca.bc.gov.backendstartapi.config.SparLog;
import ca.bc.gov.backendstartapi.security.JwtSecurityUtil;
import ca.bc.gov.backendstartapi.security.RoleAccessConfig;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import org.springframework.http.HttpStatus;
import org.springframework.lang.NonNull;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

/** This class represents a request interceptor resposible for ensuring RBAC. */
@Component
public class RoleAccessInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull Object handler)
throws Exception {
String requestUri = request.getRequestURI();

// Bypass other handlers - internal spring boot handlers
if (!handler.toString().contains("ca.bc.gov.backendstartapi")) {
return true;
}

// Gets the resource handler (class name and method name)
String[] resourceHandler = handler.toString().split("#");

// Gets the allowed roles (declared) and its allowed operations for the resource
List<String> rolesRequired = getResourceRolesRequired(resourceHandler, requestUri);

// Get the current user roles (from the request bearer token)
List<String> userRoles = getUserRoles(request);

boolean allowed = matchUserRoleWithResourceRoles(rolesRequired, userRoles);
if (!allowed) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
}

return allowed;
}

/**
* Matches the required roles with user roles.
*
* @param requiredRolesList List of roles quired for this resource.
* @param userRoles List of user roles.
* @return True if allowed, if found a matching user role, false otherwise.
*/
private boolean matchUserRoleWithResourceRoles(
List<String> requiredRolesList, List<String> userRoles) {
for (String requiredRole : requiredRolesList) {
if (userRoles.contains(requiredRole)) {
SparLog.info("Request allowed by user role: {}", requiredRole);
return true;
}
}
SparLog.info("Request denied. No enough access levels!");
return false;
}

/**
* Get user roles from the request.
*
* @param request The HTTP Request.
* @return An {@link ArrayList} of Strings with the roles, or an empty list.
*/
private List<String> getUserRoles(HttpServletRequest request) {
if (request.getUserPrincipal() instanceof JwtAuthenticationToken jwtToken) {
Set<String> roles = JwtSecurityUtil.getUserRolesFromJwt(jwtToken.getToken());
SparLog.info("User roles: {}", roles);
return new ArrayList<>(roles);
}

// Test fix!
SecurityContext context = SecurityContextHolder.getContext();
if (context.getAuthentication().getName().equals("SPARTest")) {
List<String> grantedList = new ArrayList<>();
if (context.getAuthentication().getAuthorities().size() > 0) {
Object[] grants = context.getAuthentication().getAuthorities().toArray();
for (int i = 0; i < context.getAuthentication().getAuthorities().size(); i++) {
String grant = String.valueOf(grants[i]);
grantedList.add(grant.substring(5));
}
}
SparLog.info("SPAR Test User roles: {}", grantedList);
return grantedList;
}

return List.of();
}

/**
* Gets the matrix with all declared roles and permissions to each role.
*
* @param classNameWithMethod String Array containing handlers
* @param uri Request URI.
* @return A List containing the declared roles, or empty list.
*/
private List<String> getResourceRolesRequired(String[] classNameWithMethod, String uri) {
List<String> combination = getClassAndMethodNames(classNameWithMethod);

if (combination.isEmpty()) {
return List.of();
}

String className = combination.get(0);
String methodName = combination.get(1);

try {
Class<?> handlerClass = Class.forName(className);
if (!Objects.isNull(handlerClass)) {
Method[] methods = handlerClass.getMethods();
Method method = null;
for (Method m : methods) {
if (m.getName().equals(methodName)) {
method = m;
break;
}
}
if (method == null) {
SparLog.warn("Not found method for name {} at {}!", methodName, className);
return List.of();
}
RoleAccessConfig annotation = method.getAnnotation(RoleAccessConfig.class);
if (Objects.isNull(annotation)) {
SparLog.warn("API missing CrudMatrixFilterConfigs {}#{}", className, methodName);
return List.of();
}

List<String> roles = Arrays.asList(annotation.value());
SparLog.info("Access level required for {}: {}", uri, roles);
return roles;
}
} catch (Exception e) {
SparLog.warn(
"Exception when getting roles matrix operations for {}#{}", className, methodName);
}
return List.of();
}

/**
* Gets the handler class and method name from the request.
*
* @param classNameWithMethod String Array containing handlers
* @return String arrays fixed with size 2 if found, or zero if not found
*/
private List<String> getClassAndMethodNames(String[] classNameWithMethod) {
String className = null;
if (classNameWithMethod.length > 0) {
className = classNameWithMethod[0];
}
String methodName = null;
if (classNameWithMethod.length > 1) {
int indexOfParent = classNameWithMethod[1].indexOf("(");
methodName = classNameWithMethod[1].substring(0, indexOfParent);
}

if (Objects.isNull(className) || !className.startsWith("ca.bc.gov.backendstartapi")) {
return List.of();
}

return List.of(className, methodName);
}
}
Loading

0 comments on commit 359f261

Please sign in to comment.