Skip to content

Commit 0950155

Browse files
mcmcgrath13emyl3brick-greenJordanGuinn
authored
[APP-413] feat: conditional routing to report run ui (#3100)
## Description Add gateway logic to look for `POST` to `/nbs/nfc` for report run (in the sense of opening the run page, not execution) Notes: * Added a bit more logging by default to the service ## Tickets * https://cdc-nbs.atlassian.net/browse/APP-413 ## Checklist before requesting a review - [x] PR focuses on a single story - [x] Code has been fully tested to meet acceptance criteria - [x] PR is reasonably small and reviewable (Generally less than 10 files and 500 changed lines) - [x] All new functions/classes/components reasonably small - [x] Functions/classes/components focused on one responsibility - [x] Code easy to understand and modify (clarity over concise/clever) - [x] PRs containing TypeScript follow the [Do's and Don'ts](https://www.typescriptlang.org/docs/handbook/declaration-files/do-s-and-don-ts.html) - [x] PR does not contain hardcoded values (Uses constants) - [x] All code is covered by unit or feature tests --------- Co-authored-by: elisa lee <emyl3@users.noreply.github.com> Co-authored-by: brick-green <brick.green1@gmail.com> Co-authored-by: Jordan Guinn <the.jordan.guinn@gmail.com> Co-authored-by: Jordan Guinn <jordan.guinn@skylight.digital> Co-authored-by: elisa lee <ttu8@cdc.gov> Co-authored-by: Brick Green <86254221+brick-green@users.noreply.github.com>
1 parent b7fbbb3 commit 0950155

3 files changed

Lines changed: 402 additions & 2 deletions

File tree

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
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+
}

apps/nbs-gateway/src/main/resources/application.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ nbs:
1010
uri: ${nbs.gateway.classic}
1111
base: '/nbs/HomePage.do?method=loadHomePage'
1212
ui:
13-
service: 'localhost:3000'
13+
service: ${nbs.gateway.modernization.service}
1414
uri: '${nbs.gateway.defaults.protocol}://${nbs.gateway.ui.service}'
1515
modernization:
1616
service: 'localhost:8080'
@@ -48,12 +48,16 @@ nbs:
4848

4949
logging:
5050
level:
51+
redisratelimiter: ${nbs.gateway.log.level:INFO}
5152
reactor:
5253
netty: INFO
5354
org:
5455
springframework:
5556
security: ${nbs.gateway.security.log.level:INFO}
56-
cloud.gateway.handler.RoutePredicateHandlerMapping: ${nbs.gateway.log.level:INFO}
57+
cloud.gateway: ${nbs.gateway.log.level:INFO}
58+
http.server.reactive: ${nbs.gateway.log.level:INFO}
59+
web.reactive: ${nbs.gateway.log.level:INFO}
60+
boot.autoconfigure.web: ${nbs.gateway.log.level:INFO}
5761

5862
management:
5963
health:

0 commit comments

Comments
 (0)