|
| 1 | +package gov.cdc.nbs.gateway.report; |
| 2 | + |
| 3 | +import com.fasterxml.jackson.databind.JsonNode; |
| 4 | +import gov.cdc.nbs.gateway.modernization.ModernizationService; |
| 5 | +import java.util.HashMap; |
| 6 | +import java.util.List; |
| 7 | +import java.util.function.Predicate; |
| 8 | +import org.springframework.beans.factory.annotation.Qualifier; |
| 9 | +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
| 10 | +import org.springframework.cloud.gateway.filter.GatewayFilter; |
| 11 | +import org.springframework.cloud.gateway.filter.GatewayFilterChain; |
| 12 | +import org.springframework.cloud.gateway.handler.AsyncPredicate; |
| 13 | +import org.springframework.cloud.gateway.route.RouteLocator; |
| 14 | +import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; |
| 15 | +import org.springframework.cloud.gateway.support.ServerWebExchangeUtils; |
| 16 | +import org.springframework.context.annotation.Bean; |
| 17 | +import org.springframework.context.annotation.Configuration; |
| 18 | +import org.springframework.core.Ordered; |
| 19 | +import org.springframework.http.HttpHeaders; |
| 20 | +import org.springframework.http.HttpMethod; |
| 21 | +import org.springframework.http.HttpStatus; |
| 22 | +import org.springframework.http.MediaType; |
| 23 | +import org.springframework.http.server.reactive.ServerHttpResponse; |
| 24 | +import org.springframework.util.LinkedMultiValueMap; |
| 25 | +import org.springframework.web.reactive.function.client.WebClient; |
| 26 | +import org.springframework.web.server.ServerWebExchange; |
| 27 | +import org.springframework.web.util.UriComponentsBuilder; |
| 28 | +import reactor.core.publisher.Mono; |
| 29 | + |
| 30 | +/** |
| 31 | + * Configures the NBS Report Runner to route run and execute to the modernized UI service. The |
| 32 | + * routes are only enabled when the {@code routes.report.execute.enabled} property is {@code true} |
| 33 | + * and any of the following criteria is satisfied. |
| 34 | + * |
| 35 | + * <ul> |
| 36 | + * <li>Request is a POST |
| 37 | + * <li>Path equal to {@code /nbs/nfc} |
| 38 | + * <li>Request body has ObjectType="7" |
| 39 | + * <li>Request body has OperationType="117" |
| 40 | + * <li>The referenced report has been modernized |
| 41 | + * </ul> |
| 42 | + */ |
| 43 | +@Configuration |
| 44 | +@ConditionalOnProperty( |
| 45 | + prefix = "nbs.gateway.report.execution", |
| 46 | + name = "enabled", |
| 47 | + havingValue = "true") |
| 48 | +class ReportExecutionRouteLocatorConfiguration { |
| 49 | + |
| 50 | + private static final String REPORT_OBJECT_TYPE = "7"; |
| 51 | + private static final String REPORT_OPERATION_TYPE = "117"; |
| 52 | + private static final String REPORT_MOD_RUNNER = "python"; |
| 53 | + |
| 54 | + private static final System.Logger LOGGER = |
| 55 | + System.getLogger(ReportExecutionRouteLocatorConfiguration.class.getName()); |
| 56 | + |
| 57 | + @Bean |
| 58 | + RouteLocator reportExecuteRouteLocator( |
| 59 | + final RouteLocatorBuilder builder, |
| 60 | + @Qualifier("defaults") final List<GatewayFilter> defaults, |
| 61 | + final ModernizationService modService) { |
| 62 | + return builder |
| 63 | + .routes() |
| 64 | + .route( |
| 65 | + "report-run", |
| 66 | + route -> |
| 67 | + route |
| 68 | + .order(Ordered.HIGHEST_PRECEDENCE) |
| 69 | + .method(HttpMethod.POST) |
| 70 | + .and() |
| 71 | + .path("/nbs/nfc") |
| 72 | + .and() |
| 73 | + .readBody(LinkedMultiValueMap.class, bodyPredicate()) |
| 74 | + .and() |
| 75 | + .asyncPredicate(isModPredicate(modService)) |
| 76 | + .filters(filter -> filter.filter(this::redirectToMod)) |
| 77 | + .uri("no://op")) |
| 78 | + .build(); |
| 79 | + } |
| 80 | + |
| 81 | + /** Check the /nbs/nfc post matches the object and operation for running a report */ |
| 82 | + @SuppressWarnings({"unchecked", "rawtypes"}) |
| 83 | + private Predicate<LinkedMultiValueMap> bodyPredicate() { |
| 84 | + return body -> |
| 85 | + REPORT_OBJECT_TYPE.equals(body.getFirst("ObjectType")) |
| 86 | + && REPORT_OPERATION_TYPE.equals(body.getFirst("OperationType")); |
| 87 | + } |
| 88 | + |
| 89 | + /** Query the mod API to determine if this report should be run via NBS 7 or NBS 6 */ |
| 90 | + private AsyncPredicate<ServerWebExchange> isModPredicate(final ModernizationService modService) { |
| 91 | + return exchange -> { |
| 92 | + String path = |
| 93 | + UriComponentsBuilder.fromPath("/nbs/api/report/configuration/{reportUid}/{dataSourceUid}") |
| 94 | + .uri(modService.uri()) |
| 95 | + .build() |
| 96 | + .expand(getParamsFromBody(exchange)) |
| 97 | + .toUriString(); |
| 98 | + |
| 99 | + return WebClient.create() |
| 100 | + .get() |
| 101 | + .uri(path) |
| 102 | + .accept(MediaType.APPLICATION_JSON) |
| 103 | + .cookies( |
| 104 | + // Copy the cookies from the original request onto the new request to make sure auth |
| 105 | + // works |
| 106 | + newCookies -> |
| 107 | + exchange |
| 108 | + .getRequest() |
| 109 | + .getCookies() |
| 110 | + .forEach( |
| 111 | + (name, cookies) -> |
| 112 | + cookies.forEach(cookie -> newCookies.add(name, cookie.getValue())))) |
| 113 | + .exchangeToMono( |
| 114 | + response -> { |
| 115 | + // If the api response has any set-cookie's, pass them on to overall response |
| 116 | + // so they are set in the browser. This is important for auth. |
| 117 | + exchange.getResponse().getCookies().addAll(response.cookies()); |
| 118 | + |
| 119 | + return response |
| 120 | + .bodyToMono(JsonNode.class) |
| 121 | + .flatMap( |
| 122 | + b -> { |
| 123 | + JsonNode runnerNode = b.get("runner"); |
| 124 | + if (runnerNode == null) return Mono.just(false); |
| 125 | + String runner = runnerNode.textValue(); |
| 126 | + return Mono.just(REPORT_MOD_RUNNER.equals(runner)); |
| 127 | + }); |
| 128 | + }) |
| 129 | + .doOnError( |
| 130 | + err -> |
| 131 | + LOGGER.log( |
| 132 | + System.Logger.Level.ERROR, |
| 133 | + "Error querying modernization-api for report metadata: %s" |
| 134 | + .formatted(err.getMessage()))); |
| 135 | + }; |
| 136 | + } |
| 137 | + |
| 138 | + /** |
| 139 | + * Redirect this request to the NBS 7 report UI |
| 140 | + * |
| 141 | + * <p>This is modeled after the `.redirect` method implementation, but because of the reading the |
| 142 | + * params from the body and interpolating into the location, it couldn't be used directly |
| 143 | + */ |
| 144 | + private Mono<Void> redirectToMod( |
| 145 | + final ServerWebExchange exchange, final GatewayFilterChain chain) { |
| 146 | + ServerWebExchangeUtils.setResponseStatus(exchange, HttpStatus.FOUND); |
| 147 | + |
| 148 | + HashMap<String, String> params = getParamsFromBody(exchange); |
| 149 | + String location = |
| 150 | + // Make sure the location matches the service's original request (the gateway) so the |
| 151 | + // cookies will be included on the new request to the location indicated hre |
| 152 | + UriComponentsBuilder.fromUri(exchange.getRequest().getURI()) |
| 153 | + .replacePath("/report/{reportUid}/{dataSourceUid}/run") |
| 154 | + .build() |
| 155 | + .expand(params) |
| 156 | + .toUriString(); |
| 157 | + |
| 158 | + ServerHttpResponse response = exchange.getResponse(); |
| 159 | + response.getHeaders().set(HttpHeaders.LOCATION, location); |
| 160 | + return response.setComplete(); |
| 161 | + } |
| 162 | + |
| 163 | + /** |
| 164 | + * Get the report UID and DataSource UID from the requests body and put in a hashmap. |
| 165 | + * |
| 166 | + * <p>MUST BE RUN AFTER `readBody` PREDICATE so the parsed body is cached |
| 167 | + */ |
| 168 | + @SuppressWarnings({"unchecked"}) |
| 169 | + private HashMap<String, String> getParamsFromBody(ServerWebExchange exchange) { |
| 170 | + // UPGRADE TRIPPING HAZARD: This key is a private part of the `ReadBodyRoutePredicateFactory` |
| 171 | + // implementation. It saves us needing to re-parse the cached body bytes into the map (and |
| 172 | + // deal with Mono's to do so...) |
| 173 | + LinkedMultiValueMap<String, String> body = |
| 174 | + (LinkedMultiValueMap<String, String>) |
| 175 | + exchange.getRequest().getAttributes().get("cachedRequestBodyObject"); |
| 176 | + |
| 177 | + HashMap<String, String> params = new HashMap<>(); |
| 178 | + if (body == null) { |
| 179 | + // This should never happen, but if it does will present weirdly, so leaving a breadcrumb |
| 180 | + LOGGER.log(System.Logger.Level.ERROR, "No request body found on report execution request"); |
| 181 | + return params; |
| 182 | + } |
| 183 | + params.put("reportUid", body.getFirst("ReportUID")); |
| 184 | + params.put("dataSourceUid", body.getFirst("DataSourceUID")); |
| 185 | + return params; |
| 186 | + } |
| 187 | +} |
0 commit comments