From 1bf38e8dec26998a34ebd71eabbb3bce06ad3138 Mon Sep 17 00:00:00 2001 From: Brick Green <86254221+brick-green@users.noreply.github.com> Date: Thu, 26 Sep 2024 17:07:46 -0400 Subject: [PATCH] update submission received event and delete v1 azure events (#15990) * remove ReportRouteEvent.kt * remove events and update md * update to include queryParameter * update unit test * transition to config based filtering and allow one query parameter to have multiple values * account for empty parameter filter list * transition to application.yml --- .../docs/observability/azure-events.md | 159 +++--------------- .../universal-pipeline/destination-filter.md | 8 +- .../universal-pipeline/receiver-filter.md | 3 +- .../event/ReceiverFilterFailedEvent.kt | 23 --- .../event/ReportAcceptedEvent.kt | 20 --- .../observability/event/ReportCreatedEvent.kt | 12 -- .../event/ReportNotRoutedEvent.kt | 18 -- .../observability/event/ReportRouteEvent.kt | 20 --- submissions/build.gradle.kts | 1 + .../submissions/SubmissionReceivedEvent.kt | 16 +- .../config/AllowedParametersConfig.kt | 41 +++++ .../controllers/SubmissionController.kt | 65 ++++++- .../src/main/resources/application.properties | 7 - .../src/main/resources/application.yml | 27 +++ .../SubmissionControllerIntegrationTest.kt | 6 + .../test/kotlin/SubmissionControllerTest.kt | 25 ++- 16 files changed, 189 insertions(+), 262 deletions(-) delete mode 100644 prime-router/src/main/kotlin/azure/observability/event/ReceiverFilterFailedEvent.kt delete mode 100644 prime-router/src/main/kotlin/azure/observability/event/ReportAcceptedEvent.kt delete mode 100644 prime-router/src/main/kotlin/azure/observability/event/ReportCreatedEvent.kt delete mode 100644 prime-router/src/main/kotlin/azure/observability/event/ReportNotRoutedEvent.kt delete mode 100644 prime-router/src/main/kotlin/azure/observability/event/ReportRouteEvent.kt create mode 100644 submissions/src/main/kotlin/gov/cdc/prime/reportstream/submissions/config/AllowedParametersConfig.kt delete mode 100644 submissions/src/main/resources/application.properties create mode 100644 submissions/src/main/resources/application.yml diff --git a/prime-router/docs/observability/azure-events.md b/prime-router/docs/observability/azure-events.md index 07fcbaaeb6a..d7b6b6a9bbb 100644 --- a/prime-router/docs/observability/azure-events.md +++ b/prime-router/docs/observability/azure-events.md @@ -45,115 +45,6 @@ class MyService( } ``` -Under the hood, it will serialize your event class and push the event -to the configured Microsoft AppInsights instance. - -## Event Glossery - -### ReportCreatedEvent -This event is emitted by the convert step when a report is successfully translated into a FHIR bundle. -- reportId - - The ID assigned to the created report -- topic - - The topic of the created report - - -### ReportAcceptedEvent -This event is emitted by the destination filter step, _before_ any filters are evaluated -- reportId - - The report ID from the preceding function (convert step) -- submittedReportId - - The report ID submitted by the sender -- topic - - The topic of the report -- sender - - The full sender name -- observations - - A list of observations each containing a list of its mapped conditions -- bundleSize - - Length of the bundle JSON string -- messageId - - From the bundle.identifier value and system. If ingested as HL7 this comes from MSH-10 - - -### ReportNotRoutedEvent -This is event is emitted by the destination filter step if a bundle not routed to any receivers. - -- reportId - - The ID of the empty report that terminated this lineage -- parentReportId - - The report ID from the preceding function (convert step) -- submittedReportId - - The report ID submitted by the sender -- topic - - The topic of the report -- sender - - The full sender name -- bundleSize - - Length of the bundle JSON string -- failingFilters - - A list of all the filters that failed causing this report not the be routed -- messageId - - From the bundle.identifier value and system. If ingested as HL7 this comes from MSH-10 - - -### ReportRouteEvent -This event is emitted by the receiver filter step, _after_ all filters have passed and a report has been -routed to a receiver. Many `ReportRouteEvent` can correspond to a `ReportAcceptedEvent` and can be "joined" on: - -`ReportAcceptedEvent.reportId == ReportRouteEvent.parentReportId` - -- reportId - - The ID of the report routed to the receiver -- parentReportId - - The report ID from the preceding function (destination filter step) -- submittedReportId - - The report ID submitted by the sender -- topic - - The topic of the report -- sender - - The full sender name -- receiver - - The full receiver name. (deprecated: When a report does not get routed to a receiver this value will be `"null"`) -- observations - - A list of observations each containing a list of its mapped conditions -- (deprecated) originalObservations - - (deprecated) A list of observations in the originally submitted report, before any filters were run -- filteredObservations - - A list of observations that were filtered from the bundle during filtering -- bundleSize - - Length of the bundle JSON string -- messageId - - From the bundle.identifier value and system. If ingested as HL7 this comes from MSH-10 - - -### ReceiverFilterFailedEvent -This event is emitted by the receiver filter step if a bundle fails a receiver filter. - -- reportId - - The ID of the empty report that terminated this lineage -- parentReportId - - The report ID from the preceding function (destination filter step) -- submittedReportId - - The report ID submitted by the sender -- topic - - The topic of the report -- sender - - The full sender name -- receiver - - The full receiver name. -- observations - - A list of observations each containing a list of its mapped conditions -- failingFilters - - A list of all the filters that failed for this report -- failingFilterType - - The type of filter that failed this report -- bundleSize - - Length of the bundle JSON string -- messageId - - From the bundle.identifier value and system. If ingested as HL7 this comes from MSH-10 - - ## How to query for events Events that are pushed to Azure can be found in the `customEvents` table in the log explorer. The properties defined in @@ -176,24 +67,24 @@ customEvents ### Distinct senders ``` customEvents -| where name == "ReportAcceptedEvent" -| extend sender = tostring(customDimensions.sender) -| distinct sender +| where name == "REPORT_RECEIVED" +| extend senderName = tostring(parse_json(tostring(customDimensions.params)).senderName) +| distinct senderName ``` ### Get report count sent by sender ``` customEvents -| where name == "ReportAcceptedEvent" -| extend sender = tostring(customDimensions.sender) -| summarize count() by sender +| where name == "REPORT_RECEIVED" +| extend senderName = tostring(parse_json(tostring(customDimensions.params)).senderName) +| summarize count() by senderName | order by count_ ``` ### Get report count sent by topic ``` customEvents -| where name == "ReportAcceptedEvent" +| where name == "REPORT_RECEIVED" | extend topic = tostring(customDimensions.topic) | summarize count() by topic | order by count_ @@ -202,12 +93,8 @@ customEvents ### Get reportable conditions count for all reports sent to Report Stream ``` customEvents -| where name == "ReportAcceptedEvent" -| extend observations = parse_json(tostring(customDimensions.observations)) -| mv-expand observations -| extend conditions = parse_json(tostring(observations.conditions)) -| mv-expand conditions -| extend conditionDisplay = tostring(conditions.display) +| where name == "ITEM_ACCEPTED" +| extend conditionDisplay = tostring(parse_json(tostring(parse_json(tostring(parse_json(tostring(parse_json(tostring(parse_json(tostring(customDimensions.params)).bundleDigest)).observationSummaries))[0].testSummary))[0].conditions))[0].display) | summarize count() by conditionDisplay | order by count_ ``` @@ -215,28 +102,25 @@ customEvents ### Distinct receivers ``` customEvents -| where name == "ReportRouteEvent" -| extend receiver = tostring(customDimensions.receiver) -| where receiver != "null" -| distinct receiver +| where name == "ITEM_ROUTED" +| extend receiverName = tostring(parse_json(tostring(customDimensions.params)).receiverName) +| distinct receiverName ``` ### Get report count routed to a receiver ``` customEvents -| where name == "ReportRouteEvent" -| extend receiver = tostring(customDimensions.receiver) -| where receiver != "null" -| summarize count() by receiver +| where name == "ITEM_ROUTED" +| extend receiverName = tostring(parse_json(tostring(customDimensions.params)).receiverName) +| summarize count() by receiverName | order by count_ ``` ### Get report count routed by topic ``` customEvents -| where name == "ReportRouteEvent" -| extend topic = tostring(customDimensions.topic), receiver = tostring(customDimensions.receiver) -| where receiver != "null" +| where name == "ITEM_ROUTED" +| extend topic = tostring(customDimensions.topic) | summarize count() by topic | order by count_ ``` @@ -244,13 +128,8 @@ customEvents ### Get reportable conditions count for all reports routed to receivers ``` customEvents -| where name == "ReportRouteEvent" -| extend observations = parse_json(tostring(customDimensions.observations)), receiver = tostring(customDimensions.receiver) -| where receiver != "null" -| mv-expand observations -| extend conditions = parse_json(tostring(observations.conditions)) -| mv-expand conditions -| extend conditionDisplay = tostring(conditions.display) +| where name == "ITEM_ROUTED" +| extend conditionDisplay = tostring(parse_json(tostring(parse_json(tostring(parse_json(tostring(parse_json(tostring(parse_json(tostring(customDimensions.params)).bundleDigest)).observationSummaries))[0].testSummary))[0].conditions))[0].display) | summarize count() by conditionDisplay | order by count_ ``` diff --git a/prime-router/docs/universal-pipeline/destination-filter.md b/prime-router/docs/universal-pipeline/destination-filter.md index fd9162c4d38..21fbe27e878 100644 --- a/prime-router/docs/universal-pipeline/destination-filter.md +++ b/prime-router/docs/universal-pipeline/destination-filter.md @@ -186,10 +186,10 @@ This filter will log messages to the console when: This step emits one of two events below _once_ each time it runs. -| Event | Trigger | -|-------------------------------------------------------------------------------|------------------------------------------------------------------| -| [ReportAcceptedEvent](../observability/azure-events.md#reportacceptedevent) | when a report is received by this step (precludes all filtering) | -| [ReportNotRoutedEvent](../observability/azure-events.md#reportnotroutedevent) | when a report is not valid for any receivers | +| Event | Trigger | +|-----------------|------------------------------------------------------------------| +| ITEM_ROUTED | when a report is received by this step (precludes all filtering) | +| ITEM_NOT_ROUTED | when a report is not valid for any receivers | ## Retries diff --git a/prime-router/docs/universal-pipeline/receiver-filter.md b/prime-router/docs/universal-pipeline/receiver-filter.md index 3c0de1e403d..e98957961f0 100644 --- a/prime-router/docs/universal-pipeline/receiver-filter.md +++ b/prime-router/docs/universal-pipeline/receiver-filter.md @@ -294,8 +294,7 @@ This step emits one of the below events _per receiver_ each time it runs. | Event | Trigger | |-----------------------------------------------------------------------------------------|------------------------------------------------------------| -| [ReportRouteEvent](../observability/azure-events.md#reportrouteevent) | When a report is routed to a receiver (all filters passed) | -| [ReceiverFilterFailedEvent](../observability/azure-events.md#receiverfilterfailedevent) | When a report fails receiver filters | +| ITEM_FILTER_FAILED | When a report fails receiver filters | ## Retries diff --git a/prime-router/src/main/kotlin/azure/observability/event/ReceiverFilterFailedEvent.kt b/prime-router/src/main/kotlin/azure/observability/event/ReceiverFilterFailedEvent.kt deleted file mode 100644 index 6f915d64bdf..00000000000 --- a/prime-router/src/main/kotlin/azure/observability/event/ReceiverFilterFailedEvent.kt +++ /dev/null @@ -1,23 +0,0 @@ -package gov.cdc.prime.router.azure.observability.event - -import gov.cdc.prime.router.ReportId -import gov.cdc.prime.router.ReportStreamFilterType -import gov.cdc.prime.router.Topic - -/** - * Event definition for when a report fails a receiver's filters - */ - -data class ReceiverFilterFailedEvent( - val reportId: ReportId, - val parentReportId: ReportId, - val submittedReportId: ReportId, - val topic: Topic, - val sender: String, - val receiver: String, - val observations: List, - val failingFilters: List, - val failingFilterType: ReportStreamFilterType, - val bundleSize: Int, - val messageId: AzureEventUtils.MessageID, -) : AzureCustomEvent \ No newline at end of file diff --git a/prime-router/src/main/kotlin/azure/observability/event/ReportAcceptedEvent.kt b/prime-router/src/main/kotlin/azure/observability/event/ReportAcceptedEvent.kt deleted file mode 100644 index 0eed89d94aa..00000000000 --- a/prime-router/src/main/kotlin/azure/observability/event/ReportAcceptedEvent.kt +++ /dev/null @@ -1,20 +0,0 @@ -package gov.cdc.prime.router.azure.observability.event - -import gov.cdc.prime.router.ReportId -import gov.cdc.prime.router.Topic - -/** - * Event definition for when a report is ready to be processed per receiver - * - * This event should contain all observations sent by the sender since no - * receiver specific filters have been run - */ -data class ReportAcceptedEvent( - val reportId: ReportId, - val submittedReportId: ReportId, - val topic: Topic, - val sender: String, - val observations: List, - val bundleSize: Int, - val messageId: AzureEventUtils.MessageID, -) : AzureCustomEvent \ No newline at end of file diff --git a/prime-router/src/main/kotlin/azure/observability/event/ReportCreatedEvent.kt b/prime-router/src/main/kotlin/azure/observability/event/ReportCreatedEvent.kt deleted file mode 100644 index f6453dad064..00000000000 --- a/prime-router/src/main/kotlin/azure/observability/event/ReportCreatedEvent.kt +++ /dev/null @@ -1,12 +0,0 @@ -package gov.cdc.prime.router.azure.observability.event - -import gov.cdc.prime.router.ReportId -import gov.cdc.prime.router.Topic - -/** - * An event emitted during every report created - */ -data class ReportCreatedEvent( - val reportId: ReportId, - val topic: Topic, -) : AzureCustomEvent \ No newline at end of file diff --git a/prime-router/src/main/kotlin/azure/observability/event/ReportNotRoutedEvent.kt b/prime-router/src/main/kotlin/azure/observability/event/ReportNotRoutedEvent.kt deleted file mode 100644 index 0280e12d912..00000000000 --- a/prime-router/src/main/kotlin/azure/observability/event/ReportNotRoutedEvent.kt +++ /dev/null @@ -1,18 +0,0 @@ -package gov.cdc.prime.router.azure.observability.event - -import gov.cdc.prime.router.ReportId -import gov.cdc.prime.router.Topic - -/** - * Event definition for when a report does not get routed to any receivers - */ - -data class ReportNotRoutedEvent( - val reportId: ReportId, - val parentReportId: ReportId, - val submittedReportId: ReportId, - val topic: Topic, - val sender: String, - val bundleSize: Int, - val messageId: AzureEventUtils.MessageID, -) : AzureCustomEvent \ No newline at end of file diff --git a/prime-router/src/main/kotlin/azure/observability/event/ReportRouteEvent.kt b/prime-router/src/main/kotlin/azure/observability/event/ReportRouteEvent.kt deleted file mode 100644 index 4479c753fa7..00000000000 --- a/prime-router/src/main/kotlin/azure/observability/event/ReportRouteEvent.kt +++ /dev/null @@ -1,20 +0,0 @@ -package gov.cdc.prime.router.azure.observability.event - -import gov.cdc.prime.router.ReportId -import gov.cdc.prime.router.Topic - -/** - * Event definition for when a report gets routed to a receiver - */ -data class ReportRouteEvent( - val reportId: ReportId, - val parentReportId: ReportId, - val submittedReportId: ReportId, - val topic: Topic, - val sender: String, - val receiver: String?, // TODO: this should not be nullable anymore after #14450 - val observations: List, - val filteredObservations: List, - val bundleSize: Int, - val messageId: AzureEventUtils.MessageID, -) : AzureCustomEvent \ No newline at end of file diff --git a/submissions/build.gradle.kts b/submissions/build.gradle.kts index 5c99ee9f63e..8c96e222de5 100644 --- a/submissions/build.gradle.kts +++ b/submissions/build.gradle.kts @@ -30,6 +30,7 @@ dependencies { testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") testImplementation("org.mockito.kotlin:mockito-kotlin:5.4.0") testImplementation("org.apache.commons:commons-compress:1.27.1") + testImplementation("org.springframework.security:spring-security-test") testRuntimeOnly("org.junit.platform:junit-platform-launcher") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.9.0") implementation(project(":shared")) diff --git a/submissions/src/main/kotlin/gov/cdc/prime/reportstream/submissions/SubmissionReceivedEvent.kt b/submissions/src/main/kotlin/gov/cdc/prime/reportstream/submissions/SubmissionReceivedEvent.kt index 61c348c0aa6..b82c0993a31 100644 --- a/submissions/src/main/kotlin/gov/cdc/prime/reportstream/submissions/SubmissionReceivedEvent.kt +++ b/submissions/src/main/kotlin/gov/cdc/prime/reportstream/submissions/SubmissionReceivedEvent.kt @@ -8,9 +8,17 @@ data class SubmissionReceivedEvent( val reportId: UUID, val parentReportId: UUID, val rootReportId: UUID, - val headers: Map, - val sender: String, - val senderIP: String, - val fileSize: String, + val requestParameters: SubmissionDetails, + val method: String, + val url: String, + val senderName: String, + val senderIp: String, + val fileLength: String, val blobUrl: String, + val pipelineStepName: String, +) + +data class SubmissionDetails( + val headers: Map, + val queryParameters: Map>, ) \ No newline at end of file diff --git a/submissions/src/main/kotlin/gov/cdc/prime/reportstream/submissions/config/AllowedParametersConfig.kt b/submissions/src/main/kotlin/gov/cdc/prime/reportstream/submissions/config/AllowedParametersConfig.kt new file mode 100644 index 00000000000..d3dab4cdccf --- /dev/null +++ b/submissions/src/main/kotlin/gov/cdc/prime/reportstream/submissions/config/AllowedParametersConfig.kt @@ -0,0 +1,41 @@ +package gov.cdc.prime.reportstream.submissions.config + +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.stereotype.Component + +/** + * Configuration class to load allowed headers and query parameters from the application properties. + * + * This class is used to read properties prefixed with "allowed" from the `application.properties` or `application.yml` + * file and bind them to the `headers` and `queryParameters` fields. + * + * Example of properties in the `application.properties` file: + * + * ``` + * allowed.headers.client_id=client_id + * allowed.headers.content_type=content-type + * allowed.queryParameters.processing=processing + * allowed.queryParameters.another_param=another + * ``` + * + * These properties will be automatically injected into the `headers` and `queryParameters` lists when the + * Spring application context is initialized. + * + * @property headers A list of allowed HTTP header names that are expected in incoming requests. + * @property queryParameters A list of allowed query parameter names that can be used in incoming requests. + */ +@Component +@ConfigurationProperties(prefix = "allowed") +class AllowedParametersConfig { + /** + * A list of allowed HTTP headers that can be accepted by the API. + * Each entry in the list represents a header name expected in the incoming request. + */ + var headers: List = emptyList() + + /** + * A list of allowed query parameters that the API can accept in incoming requests. + * Each entry in the list represents a query parameter name expected in the incoming request. + */ + var queryParameters: List = emptyList() +} \ No newline at end of file diff --git a/submissions/src/main/kotlin/gov/cdc/prime/reportstream/submissions/controllers/SubmissionController.kt b/submissions/src/main/kotlin/gov/cdc/prime/reportstream/submissions/controllers/SubmissionController.kt index c92e98354eb..37d92c49a74 100644 --- a/submissions/src/main/kotlin/gov/cdc/prime/reportstream/submissions/controllers/SubmissionController.kt +++ b/submissions/src/main/kotlin/gov/cdc/prime/reportstream/submissions/controllers/SubmissionController.kt @@ -8,8 +8,11 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import gov.cdc.prime.reportstream.shared.BlobUtils import gov.cdc.prime.reportstream.shared.QueueMessage import gov.cdc.prime.reportstream.shared.Submission +import gov.cdc.prime.reportstream.submissions.SubmissionDetails import gov.cdc.prime.reportstream.submissions.SubmissionReceivedEvent import gov.cdc.prime.reportstream.submissions.TelemetryService +import gov.cdc.prime.reportstream.submissions.config.AllowedParametersConfig +import jakarta.servlet.http.HttpServletRequest import org.slf4j.LoggerFactory import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity @@ -42,6 +45,7 @@ class SubmissionController( private val queueClient: QueueClient, private val tableClient: TableClient, private val telemetryService: TelemetryService, + private val allowedParametersConfig: AllowedParametersConfig, ) { /** * Submits a report. @@ -54,6 +58,7 @@ class SubmissionController( * @param contentType the content type of the report (must be "application/hl7-v2" or "application/fhir+ndjson") * @param clientId the ID of the client submitting the report. Should represent org.senderName * @param data the report data + * @param request gives access to request details * @return a ResponseEntity containing the reportID, status, and timestamp */ @PostMapping("/api/v1/reports", consumes = ["application/hl7-v2", "application/fhir+ndjson"]) @@ -65,12 +70,19 @@ class SubmissionController( @RequestHeader("x-azure-clientip") senderIp: String, @RequestHeader(value = "payloadName", required = false) payloadName: String?, @RequestBody data: String, + request: HttpServletRequest, ): ResponseEntity<*> { val reportId = UUID.randomUUID() val reportReceivedTime = Instant.now() val contentTypeMime = contentType.substringBefore(';') val status = "Received" val objectMapper = jacksonObjectMapper().registerModule(JavaTimeModule()) + // Filter request headers based on the allowed list + val filteredHeaders = filterHeaders(headers) + + // Filter query parameters based on the allowed list (only keep 'processing' or others defined in application.properties) + val filteredQueryParameters = filterQueryParameters(request) + logger.info( "Received report submission: reportId=$reportId, contentType=$contentTypeMime" + ", clientId=$clientId${payloadName?.let { ", payloadName=$it" } ?: ""}}" @@ -98,11 +110,17 @@ class SubmissionController( reportId = reportId, parentReportId = reportId, rootReportId = reportId, - headers = filterHeaders(headers), - sender = clientId, - senderIP = senderIp, - fileSize = contentLength, - blobUrl = blobClient.blobUrl + requestParameters = SubmissionDetails( + filteredHeaders, + filteredQueryParameters + ), + method = request.method, + url = request.requestURL.toString(), + senderName = clientId, + senderIp = senderIp, + fileLength = contentLength, + blobUrl = blobClient.blobUrl, + pipelineStepName = "submission" ) logger.debug("Created SUBMISSION_RECEIVED") @@ -121,7 +139,7 @@ class SubmissionController( BlobUtils.digestToString(digest), clientId.lowercase(), reportId, - filterHeaders(headers).toMap(), + filterHeaders(headers), ).serialize() logger.debug("Created message for queue") @@ -231,12 +249,41 @@ class SubmissionController( } } + /** + * Filters the request headers based on the allowed headers configured in the application.yml. + * Handles the case where allowed headers are defined as a list. + */ private fun filterHeaders(headers: Map): Map { - val headersToInclude = - listOf("client_id", "content-type", "payloadname", "x-azure-clientip", "content-length") - return headers.filter { it.key.lowercase() in headersToInclude } + val allowedHeaders = allowedParametersConfig.headers + + // Filter the request headers to only include allowed headers + return headers.filterKeys { key -> + allowedHeaders.map { it.lowercase() }.contains(key.lowercase()) + } } + /** + * Filters the query parameters based on the allowed query parameters configured in the application.yml. + * Handles multiple values for the same query parameter from HttpServletRequest. + */ + private fun filterQueryParameters(request: HttpServletRequest): Map> { + val allowedQueryParams = allowedParametersConfig.queryParameters + + // Create a map to hold the filtered query parameters + val filteredParams = mutableMapOf>() + + // Loop over allowed parameters and get their values from the request + allowedQueryParams.forEach { paramName -> + val values = request.getParameterValues(paramName) + if (values != null) { + filteredParams[paramName] = values.toList() // Convert array to List + } + } + + return filteredParams + } + + private fun formBlobName( reportId: UUID, contentTypeMime: String, diff --git a/submissions/src/main/resources/application.properties b/submissions/src/main/resources/application.properties deleted file mode 100644 index a8750014f5d..00000000000 --- a/submissions/src/main/resources/application.properties +++ /dev/null @@ -1,7 +0,0 @@ -spring.application.name=submissions -server.port=8880 -azure.storage.connection-string=${AZURE_STORAGE_CONNECTION_STRING:DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://localhost:10000/devstoreaccount1;QueueEndpoint=http://localhost:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;} -azure.storage.container-name=${AZURE_STORAGE_CONTAINER_NAME:reports} -azure.storage.queue-name=${AZURE_STORAGE_QUEUE_NAME:elr-fhir-receive} -azure.storage.table-name=${AZURE_STORAGE_TABLE_NAME:submission} -spring.security.oauth2.resourceserver.jwt.issuer-uri=https://reportstream.oktapreview.com/oauth2/ausekaai7gUuUtHda1d7 \ No newline at end of file diff --git a/submissions/src/main/resources/application.yml b/submissions/src/main/resources/application.yml new file mode 100644 index 00000000000..c75d070b3fa --- /dev/null +++ b/submissions/src/main/resources/application.yml @@ -0,0 +1,27 @@ +spring: + application: + name: submissions + security: + oauth2: + resourceserver: + jwt: + issuer-uri: https://reportstream.oktapreview.com/oauth2/ausekaai7gUuUtHda1d7 + server: + port: 8880 + +azure: + storage: + connection-string: ${AZURE_STORAGE_CONNECTION_STRING:DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://localhost:10000/devstoreaccount1;QueueEndpoint=http://localhost:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;} + container-name: ${AZURE_STORAGE_CONTAINER_NAME:reports} + queue-name: ${AZURE_STORAGE_QUEUE_NAME:elr-fhir-receive} + table-name: ${AZURE_STORAGE_TABLE_NAME:submission} + +allowed: + headers: + - client_id + - content-type + - payloadname + - x-azure-clientip + - content-length +# - queryParameters: +# - param \ No newline at end of file diff --git a/submissions/src/test/kotlin/SubmissionControllerIntegrationTest.kt b/submissions/src/test/kotlin/SubmissionControllerIntegrationTest.kt index 50255bb05e6..dbfcd9934dd 100644 --- a/submissions/src/test/kotlin/SubmissionControllerIntegrationTest.kt +++ b/submissions/src/test/kotlin/SubmissionControllerIntegrationTest.kt @@ -10,6 +10,8 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import gov.cdc.prime.reportstream.shared.QueueMessage import gov.cdc.prime.reportstream.shared.QueueMessage.ObjectMapperProvider +import gov.cdc.prime.reportstream.submissions.config.AzureConfig +import gov.cdc.prime.reportstream.submissions.config.SecurityConfig import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach @@ -17,7 +19,9 @@ import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc import org.springframework.boot.test.context.SpringBootTest +import org.springframework.context.annotation.Import import org.springframework.http.MediaType +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf import org.springframework.test.context.ActiveProfiles import org.springframework.test.context.DynamicPropertyRegistry import org.springframework.test.context.DynamicPropertySource @@ -31,6 +35,7 @@ import java.util.Base64 @SpringBootTest @ActiveProfiles("test") @AutoConfigureMockMvc +@Import(AzureConfig::class, SecurityConfig::class) class SubmissionControllerIntegrationTest { @Autowired @@ -92,6 +97,7 @@ class SubmissionControllerIntegrationTest { mockMvc.perform( MockMvcRequestBuilders.post("/api/v1/reports") + .with(csrf()) .content(requestBody) .contentType(MediaType.valueOf("application/hl7-v2")) .header("client_id", "testClient") diff --git a/submissions/src/test/kotlin/SubmissionControllerTest.kt b/submissions/src/test/kotlin/SubmissionControllerTest.kt index ab1dfd84120..2c552872834 100644 --- a/submissions/src/test/kotlin/SubmissionControllerTest.kt +++ b/submissions/src/test/kotlin/SubmissionControllerTest.kt @@ -11,7 +11,9 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import gov.cdc.prime.reportstream.shared.QueueMessage import gov.cdc.prime.reportstream.shared.QueueMessage.ObjectMapperProvider import gov.cdc.prime.reportstream.submissions.TelemetryService +import gov.cdc.prime.reportstream.submissions.config.AllowedParametersConfig import gov.cdc.prime.reportstream.submissions.config.AzureConfig +import gov.cdc.prime.reportstream.submissions.config.SecurityConfig import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -33,6 +35,7 @@ import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.boot.test.mock.mockito.MockBean import org.springframework.context.annotation.Import import org.springframework.http.MediaType +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.request.MockMvcRequestBuilders import org.springframework.test.web.servlet.result.MockMvcResultMatchers @@ -42,7 +45,7 @@ import java.util.Base64 import java.util.UUID @WebMvcTest(SubmissionController::class) -@Import(AzureConfig::class) +@Import(AzureConfig::class, SecurityConfig::class, AllowedParametersConfig::class) class SubmissionControllerTest { @Autowired @@ -126,6 +129,7 @@ class SubmissionControllerTest { mockMvc.perform( MockMvcRequestBuilders.post("/api/v1/reports") + .with(csrf()) .content(requestBody) .contentType(MediaType.valueOf("application/hl7-v2")) .header("client_id", "testClient") @@ -172,6 +176,7 @@ class SubmissionControllerTest { mockMvc.perform( MockMvcRequestBuilders.post("/api/v1/reports") + .with(csrf()) .content(requestBody) .contentType(MediaType.valueOf("application/fhir+ndjson")) .header("client_id", "testClient") @@ -203,6 +208,7 @@ class SubmissionControllerTest { mockMvc.perform( MockMvcRequestBuilders.post("/api/v1/reports") + .with(csrf()) .content(requestBody) .contentType(MediaType.APPLICATION_JSON) .header("client_id", "testClient") @@ -219,6 +225,7 @@ class SubmissionControllerTest { mockMvc.perform( MockMvcRequestBuilders.post("/api/v1/reports") + .with(csrf()) .content(requestBody) .contentType(MediaType.valueOf("application/hl7-v2")) .header("payloadname", "testPayload") @@ -241,6 +248,7 @@ class SubmissionControllerTest { mockMvc.perform( MockMvcRequestBuilders.post("/api/v1/reports") + .with(csrf()) .content(requestBody) .contentType(MediaType.parseMediaType("application/hl7-v2")) .header("client_id", "testClient") @@ -263,6 +271,7 @@ class SubmissionControllerTest { mockMvc.perform( MockMvcRequestBuilders.post("/api/v1/reports") + .with(csrf()) .content(requestBody) .contentType(MediaType.parseMediaType("application/hl7-v2")) .header("client_id", "testClient") @@ -301,11 +310,14 @@ class SubmissionControllerTest { mockMvc.perform( MockMvcRequestBuilders.post("/api/v1/reports") + .with(csrf()) .content(requestBody) .contentType(MediaType.valueOf("application/hl7-v2")) .header("client_id", "testClient") .header("payloadname", "testPayload") .header("x-azure-clientip", "127.0.0.1") + .queryParam("processing", "test1", "test2") + .queryParam("test", "test2") ) .andExpect(MockMvcResultMatchers.status().isCreated) @@ -321,12 +333,19 @@ class SubmissionControllerTest { val eventDetails = objectMapper.readValue(capturedProperties["event"], Map::class.java) assert(eventDetails["reportId"] == reportId.toString()) assert(eventDetails["blobUrl"] == expectedBlobUrl) - assert(eventDetails["senderIP"] == "127.0.0.1") - val headers = eventDetails["headers"] as Map<*, *> + assert(eventDetails["senderIp"] == "127.0.0.1") + assert(eventDetails["method"] == "POST") + assert(eventDetails["senderName"] == "testClient") + assert(eventDetails["pipelineStepName"] == "submission") + assert(eventDetails["url"] == "http://localhost/api/v1/reports") + val requestParameters = eventDetails["requestParameters"] as Map<*, *> + val headers = requestParameters["headers"] as Map<*, *> assert(headers["client_id"] == "testClient") assert(headers["Content-Type"] == "application/hl7-v2;charset=UTF-8") assert(headers["payloadname"] == "testPayload") assert(headers["x-azure-clientip"] == "127.0.0.1") + val queryParameters = requestParameters["queryParameters"] as Map<*, *> + assert(queryParameters.isEmpty()) uuidMockedStatic.close() }