Skip to content

Commit 953e8ea

Browse files
committed
feat: Authenticate with ASTP-AI Cognito API and get access token
[#OCD-4757]
1 parent 87d0de0 commit 953e8ea

9 files changed

Lines changed: 253 additions & 4 deletions

File tree

chpl/chpl-api/src/main/java/gov/healthit/chpl/ApiExceptionControllerAdvice.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import com.datadog.api.client.ApiException;
2121

22+
import gov.healthit.chpl.astpai.AstpAiRequestFailedException;
2223
import gov.healthit.chpl.auth.ChplAccountEmailNotConfirmedException;
2324
import gov.healthit.chpl.domain.error.ErrorResponse;
2425
import gov.healthit.chpl.domain.error.ObjectMissingValidationErrorResponse;
@@ -77,6 +78,14 @@ public ResponseEntity<ErrorResponse> exception(JiraRequestFailedException e) {
7778
HttpStatus.NO_CONTENT);
7879
}
7980

81+
@ExceptionHandler(AstpAiRequestFailedException.class)
82+
public ResponseEntity<ErrorResponse> exception(AstpAiRequestFailedException e) {
83+
LOGGER.error(e.getMessage());
84+
return new ResponseEntity<ErrorResponse>(
85+
new ErrorResponse("ASTP-AI information is not currently available, please check back later."),
86+
HttpStatus.NO_CONTENT);
87+
}
88+
8089
@ExceptionHandler(InsightRequestFailedException.class)
8190
public ResponseEntity<ErrorResponse> exception(InsightRequestFailedException e) {
8291
LOGGER.error(e.getMessage());

chpl/chpl-api/src/main/java/gov/healthit/chpl/web/controller/RealWorldTestingController.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
import org.springframework.web.multipart.MultipartFile;
1717

1818
import gov.healthit.chpl.FeatureList;
19+
import gov.healthit.chpl.astpai.AmazonTokenResponse;
20+
import gov.healthit.chpl.astpai.AstpAiAuthenticationService;
21+
import gov.healthit.chpl.astpai.AstpAiRequestFailedException;
1922
import gov.healthit.chpl.domain.schedule.ChplOneTimeTrigger;
2023
import gov.healthit.chpl.exception.UserRetrievalException;
2124
import gov.healthit.chpl.exception.ValidationException;
@@ -33,15 +36,18 @@
3336
@RequestMapping("/real-world-testing")
3437
public class RealWorldTestingController {
3538

39+
private AstpAiAuthenticationService authService;
3640
private RealWorldTestingManager realWorldTestingManager;
3741
private FF4j ff4j;
3842
private ServerEnvironment serverEnvironment;
3943

4044
@Autowired
4145
public RealWorldTestingController(RealWorldTestingManager realWorldTestingManager,
46+
AstpAiAuthenticationService authService,
4247
FF4j ff4j,
4348
@Value("${server.environment}") String serverEnvironment) {
4449
this.realWorldTestingManager = realWorldTestingManager;
50+
this.authService = authService;
4551
this.ff4j = ff4j;
4652
this.serverEnvironment = serverEnvironment != null ? ServerEnvironment.getByName(serverEnvironment) : null;
4753
}
@@ -81,4 +87,18 @@ public RealWorldTestingController(RealWorldTestingManager realWorldTestingManage
8187
}
8288
return realWorldTestingManager.validateResultsUrlAsBackgroundJob(request);
8389
}
90+
91+
@Operation(summary = "Create and run a background job that fetches Real World Testing validation information "
92+
+ "about any URL. The validation is expecting an RWT Results URL. Validation data will be emailed to the "
93+
+ "logged-in user.",
94+
security = {
95+
@SecurityRequirement(name = SwaggerSecurityRequirement.API_KEY),
96+
@SecurityRequirement(name = SwaggerSecurityRequirement.BEARER)
97+
})
98+
@RequestMapping(value = "/astp-ai-auth", method = RequestMethod.POST)
99+
public @ResponseBody AmazonTokenResponse authenticateToAspAi(@RequestBody RealWorldTestingResultsUrlValidationRequest request)
100+
throws AstpAiRequestFailedException {
101+
102+
return authService.authenticate();
103+
}
84104
}

chpl/chpl-resources/src/main/resources/environment.properties

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,14 @@ jira.nonconformityUrl=/search/?maxResults=100&jql=project="Review for Signals/Di
119119
insight.submissionsUrl=https://healthit-gov-develop.go-vip.net/wp-json/data-dashboard/v1/developers/%s/submissions
120120
###################################################
121121

122+
############ ASTP-AI CONNECTION PROPERTIES ###########
123+
astpai.authenticate.url=https://us-east-1zmkmlezba.auth.us-east-1.amazoncognito.com/oauth2/token
124+
astpai.authenticate.clientSecret=SECRET
125+
astpai.authenticate.clientId=SECRET
126+
astpai.domain=https://astp-dev.ainq.ai/api/
127+
astpai.rwtResultUrlValidation.endpoint=rwt-validations/from-url
128+
###################################################
129+
122130
###### CHPL-SERVICE DOWNLOAD JAR PROPERTIES ######
123131
dataSourceName=java:/comp/env/jdbc/openchpl
124132
###################################################

chpl/chpl-service/src/main/java/gov/healthit/chpl/CHPLServiceConfig.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,27 @@ public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttp
277277
return restTemplate;
278278
}
279279

280+
@Bean
281+
public RestTemplate httpsRestTemplate()
282+
throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException {
283+
CloseableHttpClient httpClient = HttpClients.custom()
284+
.setConnectionManager(PoolingHttpClientConnectionManagerBuilder.create()
285+
.setDefaultSocketConfig(SocketConfig.custom()
286+
.setSoTimeout(getRequestTimeout(), TimeUnit.MILLISECONDS)
287+
.build())
288+
.setTlsSocketStrategy(new DefaultClientTlsStrategy(
289+
SSLContexts.custom().loadTrustMaterial(TrustAllStrategy.INSTANCE).build(),
290+
NoopHostnameVerifier.INSTANCE))
291+
.build())
292+
.build();
293+
294+
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
295+
requestFactory.setHttpClient(httpClient);
296+
requestFactory.setConnectionRequestTimeout(getRequestTimeout());
297+
298+
return new RestTemplate(requestFactory);
299+
}
300+
280301
private int getRequestTimeout() {
281302
int requestTimeout = DEFAULT_REQUEST_TIMEOUT;
282303
String requestTimeoutProperty = env.getProperty("jira.requestTimeoutMillis");
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package gov.healthit.chpl.astpai;
2+
3+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4+
import com.fasterxml.jackson.annotation.JsonProperty;
5+
6+
import lombok.AllArgsConstructor;
7+
import lombok.Builder;
8+
import lombok.Data;
9+
import lombok.NoArgsConstructor;
10+
11+
@Data
12+
@NoArgsConstructor
13+
@AllArgsConstructor
14+
@Builder
15+
@JsonIgnoreProperties(ignoreUnknown = true)
16+
public class AmazonTokenResponse {
17+
@JsonProperty("access_token")
18+
private String accessToken;
19+
20+
@JsonProperty("id_token")
21+
private String idToken;
22+
23+
@JsonProperty("refresh_token")
24+
private String refreshToken;
25+
26+
@JsonProperty("token_type")
27+
private String tokenType;
28+
29+
@JsonProperty("expires_in")
30+
private Integer expiresIn;
31+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package gov.healthit.chpl.astpai;
2+
3+
import org.springframework.beans.factory.annotation.Autowired;
4+
import org.springframework.beans.factory.annotation.Value;
5+
import org.springframework.http.HttpEntity;
6+
import org.springframework.http.HttpHeaders;
7+
import org.springframework.http.HttpMethod;
8+
import org.springframework.http.HttpStatus;
9+
import org.springframework.http.HttpStatusCode;
10+
import org.springframework.http.ResponseEntity;
11+
import org.springframework.stereotype.Service;
12+
import org.springframework.web.client.HttpClientErrorException;
13+
import org.springframework.web.client.RestTemplate;
14+
15+
import lombok.extern.log4j.Log4j2;
16+
import tools.jackson.core.JacksonException;
17+
import tools.jackson.databind.json.JsonMapper;
18+
19+
@Log4j2
20+
@Service
21+
public class AstpAiAuthenticationService {
22+
23+
private RestTemplate httpsRestTemplate;
24+
private String authenticationUrl;
25+
private String authenticationRequestBody;
26+
private JsonMapper jsonMapper;
27+
28+
@Autowired
29+
public AstpAiAuthenticationService(RestTemplate httpsRestTemplate,
30+
JsonMapper jsonMapper,
31+
@Value("${astpai.authenticate.url}") String authenticationUrl,
32+
@Value("${astpai.authenticate.clientSecret}") String authenticationClientSecret,
33+
@Value("${astpai.authenticate.clientId}") String authenticationClientId) {
34+
this.httpsRestTemplate = httpsRestTemplate;
35+
this.jsonMapper = jsonMapper;
36+
this.authenticationUrl = authenticationUrl;
37+
this.authenticationRequestBody = String.format("grant_type=client_credentials&scope=default-m2m-resource-server-p3thsy/read&client_id=%s&client_secret=%s", authenticationClientId, authenticationClientSecret);
38+
}
39+
40+
public AmazonTokenResponse authenticate() throws AstpAiRequestFailedException {
41+
LOGGER.info("Making request to " + authenticationUrl);
42+
ResponseEntity<String> response = null;
43+
try {
44+
LOGGER.debug("Request body:" + authenticationRequestBody);
45+
HttpHeaders headers = new HttpHeaders();
46+
headers.add("Content-Type", "application/x-www-form-urlencoded");
47+
headers.add("Accept", "application/json");
48+
headers.add("Accept-Encoding", "UTF-8");
49+
HttpEntity<String> entity = new HttpEntity<>(authenticationRequestBody, headers);
50+
51+
response = httpsRestTemplate.exchange(authenticationUrl, HttpMethod.POST, entity, String.class);
52+
LOGGER.debug("Response: " + response.getBody());
53+
} catch (HttpClientErrorException httpEx) {
54+
LOGGER.error("Unable to authenticate with the URL " + authenticationUrl + ". Message: " + httpEx.getMessage() + "; response status code " + httpEx.getStatusCode());
55+
throw new AstpAiRequestFailedException(httpEx.getMessage(), httpEx, httpEx.getStatusCode());
56+
} catch (Exception ex) {
57+
HttpStatusCode statusCode = (response != null && response.getStatusCode() != null
58+
? response.getStatusCode() : HttpStatusCode.valueOf(HttpStatus.INTERNAL_SERVER_ERROR.value()));
59+
LOGGER.error("Unable to authenticate with the URL " + authenticationUrl + ". Message: " + ex.getMessage() + "; response status code " + statusCode);
60+
throw new AstpAiRequestFailedException(ex.getMessage(), ex, statusCode);
61+
}
62+
63+
String responseBody = response == null ? "" : response.getBody();
64+
AmazonTokenResponse token = null;
65+
try {
66+
token = jsonMapper.readValue(responseBody, AmazonTokenResponse.class);
67+
} catch (JacksonException ex) {
68+
LOGGER.error("Unable to read the response body as our custom AmazonTokenResponse", ex);
69+
throw new AstpAiRequestFailedException(ex.getMessage(), ex);
70+
}
71+
return token;
72+
}
73+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package gov.healthit.chpl.astpai;
2+
3+
import org.springframework.stereotype.Service;
4+
5+
@Service
6+
public class AstpAiQueryService {
7+
8+
//TODO methods to query the AI tool
9+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package gov.healthit.chpl.astpai;
2+
3+
import java.io.IOException;
4+
5+
import org.springframework.http.HttpStatusCode;
6+
7+
import lombok.Data;
8+
9+
@Data
10+
public class AstpAiRequestFailedException extends IOException {
11+
private static final long serialVersionUID = 3861221517156321545L;
12+
private HttpStatusCode statusCode;
13+
14+
public AstpAiRequestFailedException() {
15+
super();
16+
}
17+
18+
public AstpAiRequestFailedException(String message) {
19+
super(message);
20+
}
21+
22+
public AstpAiRequestFailedException(String message, HttpStatusCode statusCode) {
23+
super(message);
24+
this.statusCode = statusCode;
25+
}
26+
27+
public AstpAiRequestFailedException(String message, Throwable cause) {
28+
super(message, cause);
29+
}
30+
31+
public AstpAiRequestFailedException(String message, Throwable cause, HttpStatusCode statusCode) {
32+
super(message, cause);
33+
this.statusCode = statusCode;
34+
}
35+
36+
public AstpAiRequestFailedException(Throwable cause) {
37+
super(cause);
38+
}
39+
40+
public AstpAiRequestFailedException(Throwable cause, HttpStatusCode statusCode) {
41+
super(cause);
42+
this.statusCode = statusCode;
43+
}
44+
}

chpl/chpl-service/src/main/java/gov/healthit/chpl/scheduler/job/realworldtesting/RealWorldTestingUrlValidationJob.java

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
import org.springframework.core.env.Environment;
1111
import org.springframework.web.context.support.SpringBeanAutowiringSupport;
1212

13+
import gov.healthit.chpl.astpai.AmazonTokenResponse;
14+
import gov.healthit.chpl.astpai.AstpAiAuthenticationService;
15+
import gov.healthit.chpl.astpai.AstpAiRequestFailedException;
1316
import gov.healthit.chpl.auth.user.JWTAuthenticatedUser;
1417
import gov.healthit.chpl.email.ChplEmailFactory;
1518
import gov.healthit.chpl.email.ChplHtmlEmailBuilder;
@@ -45,6 +48,9 @@ public class RealWorldTestingUrlValidationJob extends QuartzJob {
4548
@Value("${surveillance.quarterlyReport.failure.subject}")
4649
private String quarterlyReportFailureSubject;
4750

51+
@Autowired
52+
private AstpAiAuthenticationService astpAiService;
53+
4854
@Autowired
4955
private ErrorMessageUtil msgUtil;
5056

@@ -70,16 +76,25 @@ public void execute(JobExecutionContext jobContext) throws JobExecutionException
7076
Integer year = (Integer) jobDataMap.get(YEAR_KEY);
7177
setSecurityContext(user);
7278

73-
//TODO ensure URL type is RESULTS
79+
//authenticate
80+
AmazonTokenResponse token = null;
81+
try {
82+
token = astpAiService.authenticate();
83+
} catch (AstpAiRequestFailedException ex) {
84+
LOGGER.error("Unable to authenticate with ASTP-AI", ex);
85+
sendErrorEmail(user.getEmail(), quarterlyReportFailureSubject,
86+
env.getProperty("surveillance.quarterlyReport.badJobData.htmlBody"));
87+
}
7488
//TODO call AI endpoint, get response or handle error
89+
7590
//TODO parse results and send email
76-
sendEmail(user.getEmail(), quarterlyReportFailureSubject,
91+
sendResultsEmail(user.getEmail(), quarterlyReportFailureSubject,
7792
env.getProperty("surveillance.quarterlyReport.fileError.htmlBody"));
7893

7994
} else {
8095
JWTAuthenticatedUser user = (JWTAuthenticatedUser) jobDataMap.get(USER_KEY);
8196
if (user != null && user.getEmail() != null) {
82-
sendEmail(user.getEmail(), quarterlyReportFailureSubject,
97+
sendErrorEmail(user.getEmail(), quarterlyReportFailureSubject,
8398
env.getProperty("surveillance.quarterlyReport.badJobData.htmlBody"));
8499
}
85100
}
@@ -126,7 +141,26 @@ private boolean isJobDataValid(JobDataMap jobDataMap) {
126141
return isValid;
127142
}
128143

129-
private void sendEmail(String recipientEmail, String subject, String htmlContent) {
144+
private void sendResultsEmail(String recipientEmail, String subject, String htmlContent) {
145+
LOGGER.info("Sending email to: " + recipientEmail);
146+
147+
try {
148+
chplEmailFactory.emailBuilder()
149+
.recipient(recipientEmail)
150+
.subject(subject)
151+
.htmlMessage(chplHtmlEmailBuilder.initialize()
152+
.heading(subject)
153+
.paragraph("", htmlContent)
154+
.paragraph("", String.format(chplEmailValediction, acbatlFeedbackUrl))
155+
.footer(AdminFooter.class)
156+
.build())
157+
.sendEmail();
158+
} catch (EmailNotSentException ex) {
159+
LOGGER.error("Could not send email to " + recipientEmail, ex);
160+
}
161+
}
162+
163+
private void sendErrorEmail(String recipientEmail, String subject, String htmlContent) {
130164
LOGGER.info("Sending email to: " + recipientEmail);
131165

132166
try {

0 commit comments

Comments
 (0)