Skip to content

Commit

Permalink
Merge branch 'ver-47.6.0' into production
Browse files Browse the repository at this point in the history
  • Loading branch information
kekey1 committed Dec 18, 2024
2 parents 0895014 + 0d1047f commit 12f22e5
Show file tree
Hide file tree
Showing 61 changed files with 851 additions and 246 deletions.
17 changes: 17 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# Release Notes

## Version 47.6.0
_18 December 2024_

### Features
* Add endpoint to return data for Service Base URL List report
* Allow existing user to be granted access to additional Organizations
* Return generic msg if invitation token is bad
* Create /developers/<id>/insights endpoint to fetch insights data

### Bug Fixes
* Add all required standards to listing as of cert day + current day
* Give appropriate error if addt'l software group name too long
* Give pending change request report user correct cognito group
* Send API Key deletion warning if key was created and never used

---

## Version 47.5.0
_9 December 2024_

Expand Down
50 changes: 50 additions & 0 deletions chpl/chpl-api/e2e/collections/report-data.postman_collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,56 @@
"description": "GET /report-data/standards-listing - 200 status and response fields validation"
},
"response": []
},
{
"name": "GET /report-data/service-base-url-list - 200 status and valid response",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"/report-data/service-base-url-list should retrieve status 200 and valid response\", function () {\r",
" pm.response.to.have.status(200);\r",
" pm.expect(pm.response.text()).to.include(\"urlUptimeMonitorId\");\r",
" pm.expect(pm.response.text()).to.include(\"datadogTestKey\");\r",
" pm.expect(pm.response.text()).to.include(\"checkTime\");\r",
" pm.expect(pm.response.text()).to.include(\"passed\");\r",
"});\r",
""
],
"type": "text/javascript",
"packages": {}
}
}
],
"request": {
"method": "GET",
"header": [
{
"key": "API-Key",
"value": "{{apiKey}}",
"type": "text"
},
{
"key": "Content-Type",
"value": "application/json",
"type": "text"
}
],
"url": {
"raw": "{{url}}/rest/report-data/service-base-url-list",
"host": [
"{{url}}"
],
"path": [
"rest",
"report-data",
"service-base-url-list"
]
},
"description": "/report-data/service-base-url-list should retrieve status 200 and valid response"
},
"response": []
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import gov.healthit.chpl.exception.UserAccountExistsException;
import gov.healthit.chpl.exception.UserRetrievalException;
import gov.healthit.chpl.exception.ValidationException;
import gov.healthit.chpl.insight.InsightRequestFailedException;
import gov.healthit.chpl.manager.impl.UpdateCertifiedBodyException;
import gov.healthit.chpl.manager.impl.UpdateTestingLabException;
import gov.healthit.chpl.user.cognito.authentication.CognitoAuthenticationChallenge;
Expand Down Expand Up @@ -74,6 +75,14 @@ public ResponseEntity<ErrorResponse> exception(JiraRequestFailedException e) {
HttpStatus.NO_CONTENT);
}

@ExceptionHandler(InsightRequestFailedException.class)
public ResponseEntity<ErrorResponse> exception(InsightRequestFailedException e) {
LOGGER.error(e.getMessage());
return new ResponseEntity<ErrorResponse>(
new ErrorResponse("Insights information is not currently available, please check back later."),
e.getStatusCode() != null ? e.getStatusCode() : HttpStatus.NO_CONTENT);
}

@ExceptionHandler(EntityRetrievalException.class)
public ResponseEntity<ErrorResponse> exception(EntityRetrievalException e) {
LOGGER.error(e.getMessage());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ public void setPassword(@RequestBody CognitoUpdatePasswordRequest request) throw

@RequestMapping(value = "/refresh-token", method = RequestMethod.POST,
produces = "application/json; charset=utf-8")
public CognitoAuthenticationResponse refreshToken(@RequestBody CognitoRefreshTokenRequest request) {
public CognitoAuthenticationResponse refreshToken(@RequestBody CognitoRefreshTokenRequest request) throws UserRetrievalException {
if (!ff4j.check(FeatureList.SSO)) {
throw new NotImplementedException("This method has not been implemented");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.NotImplementedException;
import org.apache.commons.lang3.StringUtils;
import org.ff4j.FF4j;
import org.quartz.SchedulerException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
Expand All @@ -22,6 +24,7 @@

import com.fasterxml.jackson.core.JsonProcessingException;

import gov.healthit.chpl.FeatureList;
import gov.healthit.chpl.attestation.domain.AttestationPeriodDeveloperException;
import gov.healthit.chpl.attestation.manager.AttestationManager;
import gov.healthit.chpl.caching.CacheNames;
Expand All @@ -43,6 +46,9 @@
import gov.healthit.chpl.exception.InvalidArgumentsException;
import gov.healthit.chpl.exception.JiraRequestFailedException;
import gov.healthit.chpl.exception.ValidationException;
import gov.healthit.chpl.insight.InsightRequestFailedException;
import gov.healthit.chpl.insight.InsightService;
import gov.healthit.chpl.insight.InsightSubmission;
import gov.healthit.chpl.manager.CertifiedProductManager;
import gov.healthit.chpl.manager.DeveloperManager;
import gov.healthit.chpl.manager.UserPermissionsManager;
Expand Down Expand Up @@ -70,23 +76,29 @@ public class DeveloperController {
private ErrorMessageUtil msgUtil;
private UserPermissionsManager userPermissionsManager;
private AttestationManager attestationManager;
private InsightService insightsService;
private DirectReviewCachingService directReviewService;
private RealWorldTestingManager rwtManager;
private FF4j ff4j;

@Autowired
public DeveloperController(DeveloperManager developerManager,
CertifiedProductManager cpManager,
UserPermissionsManager userPermissionsManager,
AttestationManager attestationManager,
InsightService insightsService,
ErrorMessageUtil msgUtil,
DirectReviewCachingService directReviewService,
RealWorldTestingManager rwtManager) {
RealWorldTestingManager rwtManager,
FF4j ff4j) {
this.developerManager = developerManager;
this.userPermissionsManager = userPermissionsManager;
this.attestationManager = attestationManager;
this.insightsService = insightsService;
this.msgUtil = msgUtil;
this.directReviewService = directReviewService;
this.rwtManager = rwtManager;
this.ff4j = ff4j;
}

@DeprecatedApiResponseFields(friendlyUrl = "/developers", httpMethod = "GET", responseClass = DeveloperResults.class)
Expand Down Expand Up @@ -140,6 +152,20 @@ public DeveloperController(DeveloperManager developerManager,
directReviewService.getDirectReviews(developerId).getDirectReviews(), HttpStatus.OK);
}

@Operation(summary = "List Insight sumbissions for a developer.",
security = {
@SecurityRequirement(name = SwaggerSecurityRequirement.API_KEY),
@SecurityRequirement(name = SwaggerSecurityRequirement.BEARER)
})
@RequestMapping(value = "/{developerId}/insights", method = RequestMethod.GET, produces = "application/json; charset=utf-8")
public @ResponseBody ResponseEntity<List<InsightSubmission>> getInsights(@PathVariable("developerId") Long developerId)
throws InsightRequestFailedException, EntityRetrievalException {
if (!ff4j.check(FeatureList.INSIGHTS)) {
throw new NotImplementedException("This method has not been implemented");
}
return new ResponseEntity<List<InsightSubmission>>(insightsService.getInsightSubmissions(developerId), HttpStatus.OK);
}

@Operation(summary = "List all Real World Testing Plans URLs from active certificates for a developer.",
security = {
@SecurityRequirement(name = SwaggerSecurityRequirement.API_KEY)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import gov.healthit.chpl.report.listing.UniqueListingCount;
import gov.healthit.chpl.report.product.ProductByAcb;
import gov.healthit.chpl.report.product.UniqueProductCount;
import gov.healthit.chpl.report.servicebaseurllistreport.UrlUptimeMonitorEx;
import gov.healthit.chpl.report.surveillance.CapCounts;
import gov.healthit.chpl.report.surveillance.NonconformityCounts;
import gov.healthit.chpl.report.surveillance.SurveillanceActivityCounts;
Expand Down Expand Up @@ -421,6 +422,16 @@ public ReportDataController(ReportDataManager reportDataManager, DeveloperSearch
return reportDataManager.getTestToolListingReports();
}

@Operation(summary = "Retrieves the data used to generate the Service Base Url List report.",
description = "Retrieves the data used to generate the Service Base Url List report.",
security = {
@SecurityRequirement(name = SwaggerSecurityRequirement.API_KEY)
})
@RequestMapping(value = "/service-base-url-list", method = RequestMethod.GET, produces = "application/json; charset=utf-8")
public @ResponseBody List<UrlUptimeMonitorEx> getUrlUptimeMonitors() {
return reportDataManager.getUrlUptimeMonitors();
}

@Operation(summary = "Retrieves the data used to generate the Standard Criteria Attribute Summary report.",
description = "Retrieves the data used to generate the Standard Criteria Attribute Summary report.",
security = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
Expand Down Expand Up @@ -70,10 +71,10 @@
import lombok.Getter;
import lombok.extern.log4j.Log4j2;

@Log4j2
@Tag(name = "users", description = "Allows management of users.")
@RestController
@RequestMapping("/users")
@Log4j2
public class UserManagementController {
private UserManager userManager;
private InvitationManager invitationManager;
Expand Down Expand Up @@ -110,13 +111,30 @@ public UserManagementController(UserManager userManager, InvitationManager invit
this.authorizationLengthInDays = authorizationLengthInDays;
}

@Operation(summary = "View a specific user's details.",
description = "The logged in user must either be the user in the parameters, have ROLE_ADMIN, or "
+ "have ROLE_ACB.",
@Operation(summary = "Update the currently logged in user with an additional organization.",
description = "Update the currently logged in user with an additional organization. This"
+ "is typically adding another developer or ONC-ACB to an existing user's list "
+ "of organizations they have access to.",
security = {
@SecurityRequirement(name = SwaggerSecurityRequirement.API_KEY),
@SecurityRequirement(name = SwaggerSecurityRequirement.BEARER)
})
@RequestMapping(value = "/authorize/{invitationToken}", method = RequestMethod.POST,
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = "application/json; charset=utf-8")
public User addOrganizationToUser(@PathVariable("invitationToken") UUID invitationToken, @RequestHeader("authorization") String jwt)
throws UserRetrievalException, InvalidArgumentsException, ActivityException {

return cognitoUserManager.addOrganizationToUser(invitationToken, jwt.split(" ")[1]);
}

@Operation(summary = "View a specific user's details.",
description = "The logged in user must either be the user in the parameters, have ROLE_ADMIN, or "
+ "have ROLE_ACB.",
security = {
@SecurityRequirement(name = SwaggerSecurityRequirement.API_KEY),
@SecurityRequirement(name = SwaggerSecurityRequirement.BEARER)
})
@RequestMapping(value = "/{cognitoUserId}", method = RequestMethod.GET,
produces = "application/json; charset=utf-8")
public @ResponseBody User getUser(@PathVariable("cognitoUserId") UUID cognitoUserId) throws UserRetrievalException {
Expand All @@ -127,14 +145,13 @@ public UserManagementController(UserManager userManager, InvitationManager invit
return cognitoUserManager.getUserInfo(cognitoUserId);
}


@Operation(summary = "Invite a user to the CHPL.",
description = "This request creates an invitation that is sent to the email address provided. "
+ "The recipient of this invitation can then choose to create a new account "
+ "or add the permissions contained within the invitation to an existing account "
+ "if they have one. Said another way, an invitation can be used to create or "
+ "modify CHPL user accounts." + "The correct order to call invitation requests is "
+ "the following: 1) /invite 2) /create or /authorize. "
+ "the following: 1) POST /users/invitation 2) POST /users or POST users/authorize/{invitationToken}. "
+ "Security Restrictions: ROLE_ADMIN and ROLE_ONC can invite users to any organization. "
+ "ROLE_ACB can add users to their own organization.",
security = {
Expand Down Expand Up @@ -177,25 +194,44 @@ public CognitoUserInvitation inviteUser(@RequestBody CognitoUserInvitation invit
+ "That user key along with all the information needed to create a new user's account "
+ "can be passed in here. The account is created but cannot be used until that user "
+ "confirms that their email address is valid. The correct order to call invitation requests is "
+ "the following: 1) /invite 2) /create or /authorize ",
+ "the following: 1) POST /users/invitation 2) POST /users or POST users/authorize/{invitationToken}",
security = {
@SecurityRequirement(name = SwaggerSecurityRequirement.API_KEY)
})
@RequestMapping(value = "", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE,
produces = "application/json; charset=utf-8")
public void addUser(@RequestBody CreateUserFromInvitationRequest userInfo) throws ValidationException, EmailNotSentException,
UserRetrievalException, UserCreationException, ActivityException {
public void addUser(@RequestBody CreateUserFromInvitationRequest userInfo) throws InvalidArgumentsException,
ValidationException, EmailNotSentException, UserRetrievalException, UserCreationException, ActivityException {
if (!ff4j.check(FeatureList.SSO)) {
throw new NotImplementedException("This method has not been implemented");
}
UUID token = null;

try {
CognitoUserInvitation invitation = cognitoInvitationManager.getByToken(UUID.fromString(userInfo.getHash()));
token = UUID.fromString(userInfo.getHash());
} catch (IllegalArgumentException ex) {
LOGGER.error("Attempting to create a user from a invalid invitation token: " + userInfo.getHash(), ex);
throw new InvalidArgumentsException(msgUtil.getMessage("user.invitation.invalid",
authorizationLengthInDays + "",
authorizationLengthInDays == 1 ? "" : "s"));
}

try {
CognitoUserInvitation invitation = cognitoInvitationManager.getByToken(token);
if (invitation != null) {
cognitoUserManager.createUser(userInfo);
} else {
throw new InvalidArgumentsException(msgUtil.getMessage("user.invitation.invalid",
authorizationLengthInDays + "",
authorizationLengthInDays == 1 ? "" : "s"));
}
} catch (ValidationException ex) {
throw ex;
} catch (Exception ex) {
LOGGER.error("Error creating user from invitation.", ex);
throw new InvalidArgumentsException(msgUtil.getMessage("user.invitation.invalid",
authorizationLengthInDays + "",
authorizationLengthInDays == 1 ? "" : "s"));
} finally {
SecurityContextHolder.getContext().setAuthentication(null);
}
Expand Down Expand Up @@ -225,6 +261,7 @@ public User updateUserDetails(@RequestBody User userInfo, @PathVariable("cognito

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////


@Deprecated
@DeprecatedApi(friendlyUrl = "/users/create",
httpMethod = "POST",
Expand Down Expand Up @@ -257,7 +294,7 @@ public User updateUserDetails(@RequestBody User userInfo, @PathVariable("cognito

UserInvitation invitation = invitationManager.getByInvitationHash(userInfo.getHash());
if (invitation == null || invitation.isOlderThan(invitationLengthInDays)) {
throw new ValidationException(msgUtil.getMessage("user.invitation.expired",
throw new ValidationException(msgUtil.getMessage("user.invitation.invalid",
invitationLengthInDays + "",
invitationLengthInDays == 1 ? "" : "s"));
}
Expand Down Expand Up @@ -360,7 +397,7 @@ public String authorizeUser(@RequestBody AuthorizeCredentials credentials)

UserInvitation invitation = invitationManager.getByInvitationHash(credentials.getHash());
if (invitation == null || invitation.isOlderThan(authorizationLengthInDays)) {
throw new InvalidArgumentsException(msgUtil.getMessage("user.invitation.expired",
throw new InvalidArgumentsException(msgUtil.getMessage("user.invitation.invalid",
authorizationLengthInDays + "",
authorizationLengthInDays == 1 ? "" : "s"));
}
Expand Down
2 changes: 1 addition & 1 deletion chpl/chpl-resources/src/main/resources/email.properties
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ noRecipientsErrorEmailBody=<p><b>An email with subject: '%s' attempted to be sen
#DeleteAPIKeyWarningEmail
job.apiKeyWarningEmailJob.config.apiKeyLastUsedDaysAgo=90
job.apiKeyWarningEmailJob.config.daysUntilDelete=90
job.apiKeyWarningEmailJob.config.message=Name/Organization: %s<br/><br/>Unused API keys are removed from the CHPL system after %s days. Your key, %s, was last used on %s and unless it is used again, will be removed in %s days.<br/><br/>Thank you
job.apiKeyWarningEmailJob.config.message=Name/Organization: %s<br/><br/>Unused API keys are removed from the CHPL system after %s days. Your key, %s, was last used on %s and unless it is used again, will be removed in %s days.<br/><br/>Thank you
job.apiKeyWarningEmailJob.config.subject=ONC-CHPL: Your API key will be deleted

#ApiKeyDeleteJob
Expand Down
Loading

0 comments on commit 12f22e5

Please sign in to comment.