Skip to content

Commit

Permalink
Merge pull request #8056 from CDCgov/platform/7927
Browse files Browse the repository at this point in the history
Update validation endpoint and remove authentication
  • Loading branch information
arnejduranovic authored Jan 27, 2023
2 parents 8b92a1e + c3b373c commit b62c83d
Show file tree
Hide file tree
Showing 5 changed files with 251 additions and 99 deletions.
67 changes: 67 additions & 0 deletions prime-router/docs/api/validate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
openapi: 3.0.2
info:
title: Prime ReportStream
description: A router of public health data from multiple senders and receivers
contact:
name: USDS at Centers for Disease Control and Prevention
url: https://reportstream.cdc.gov
email: reportstream@cdc.gov
version: 0.2.0-oas3
tags:
- name: validate
description: ReportStream validation API

paths:
# The validation endpoints are public endpoints for validating payloads of various formats.
/validate:
post:
tags:
- validate
summary: Validate a message using client information
parameters:
- in: header
name: client
description: The client.sender to validate against. If client is not known, use `schema` and `format` instead.
schema:
type: string
example: simple_report.default
- in: query
name: schema
description: >
The schema path to validate the message against. Must be use with `format`.
This parameter is incompatible with `client`.
schema:
type: string
example: hl7/hcintegrations-covid-19
- in: query
name: format
description: >
The format of the message. must be used with `schema`.
This parameter is incompatible with `client`.
schema:
type: string
enum:
- CSV
- HL7
- HL7_BATCH
example: HL7
requestBody:
description: The message to validate
required: true
content:
text/csv:
schema:
type: string
example:
header1, header2

value1, value2
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: 'https://raw.githubusercontent.com/CDCgov/prime-reportstream/master/prime-router/docs/api/reports.yml#/components/schemas/Report'
'400':
description: Bad Request
11 changes: 11 additions & 0 deletions prime-router/docs/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@
- The ReportStream API is documented here: [Hub OpenApi Spec](./api)
- More detailed changelog for individual releases: [Recent releases](https://github.com/CDCgov/prime-reportstream/releases)

## January 31, 2023

### Make validation endpoint public

This release removes authentication/authorization from the `/api/validate` endpoint.

Previously, the `client` parameter had to be set in the header. This is still an option, but now, the client
can pass `schema` and `format` as query parameters and a schema matching those values will be used to
validate the message. Additional information can be found in the API documentation.


## November 29, 2022

### Change to the api/token endpoint
Expand Down
68 changes: 60 additions & 8 deletions prime-router/src/main/kotlin/azure/RequestFunction.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ package gov.cdc.prime.router.azure
import com.google.common.net.HttpHeaders
import com.microsoft.azure.functions.HttpRequestMessage
import gov.cdc.prime.router.ActionLogger
import gov.cdc.prime.router.CustomerStatus
import gov.cdc.prime.router.DEFAULT_SEPARATOR
import gov.cdc.prime.router.HasSchema
import gov.cdc.prime.router.InvalidParamMessage
import gov.cdc.prime.router.ROUTE_TO_SEPARATOR
import gov.cdc.prime.router.Schema
import gov.cdc.prime.router.Sender
import gov.cdc.prime.router.TopicSender
import java.lang.IllegalArgumentException
import java.security.InvalidParameterException

const val CLIENT_PARAMETER = "client"
const val PAYLOAD_NAME_PARAMETER = "payloadname"
Expand All @@ -17,6 +21,8 @@ const val DEFAULT_PARAMETER = "default"
const val ROUTE_TO_PARAMETER = "routeTo"
const val ALLOW_DUPLICATES_PARAMETER = "allowDuplicate"
const val TOPIC_PARAMETER = "topic"
const val SCHEMA_PARAMETER = "schema"
const val FORMAT_PARAMETER = "format"

/**
* Base class for ReportFunction and ValidateFunction
Expand All @@ -31,7 +37,7 @@ abstract class RequestFunction(
val content: String = "",
val defaults: Map<String, String> = emptyMap(),
val routeTo: List<String> = emptyList(),
val sender: Sender,
val sender: Sender?,
val topic: String = "covid-19"
)

Expand Down Expand Up @@ -70,14 +76,26 @@ abstract class RequestFunction(
routeTo.filter { workflowEngine.settings.findReceiver(it) == null }
.forEach { actionLogs.error(InvalidParamMessage("Invalid receiver name: $it")) }

var sender: Sender? = null
val clientName = extractClient(request)
if (clientName.isBlank()) {
actionLogs.error(InvalidParamMessage("Expected a '$CLIENT_PARAMETER' query parameter"))
}

val sender = workflowEngine.settings.findSender(clientName)
if (sender == null) {
actionLogs.error(InvalidParamMessage("'$CLIENT_PARAMETER:$clientName': unknown sender"))
// Find schema via SCHEMA_PARAMETER parameter
try {
sender = getDummySender(
request.queryParameters.getOrDefault(SCHEMA_PARAMETER, null),
request.queryParameters.getOrDefault(FORMAT_PARAMETER, null)
)
} catch (e: InvalidParameterException) {
actionLogs.error(
InvalidParamMessage(e.message.toString())
)
}
} else {
// Find schema via CLIENT_PARAMETER parameter
sender = workflowEngine.settings.findSender(clientName)
if (sender == null) {
actionLogs.error(InvalidParamMessage("'$CLIENT_PARAMETER:$clientName': unknown sender"))
}
}

// verify schema if the sender is a topic sender
Expand All @@ -86,11 +104,12 @@ abstract class RequestFunction(
schema = workflowEngine.metadata.findSchema(sender.schemaName)
if (schema == null) {
actionLogs.error(
InvalidParamMessage("'$CLIENT_PARAMETER:$clientName': unknown schema '${sender.schemaName}'")
InvalidParamMessage("unknown schema '${sender.schemaName}'")
)
}
}

// validate content type
val contentType = request.headers.getOrDefault(HttpHeaders.CONTENT_TYPE.lowercase(), "")
if (contentType.isBlank()) {
actionLogs.error(InvalidParamMessage("Missing ${HttpHeaders.CONTENT_TYPE} header"))
Expand Down Expand Up @@ -151,4 +170,37 @@ abstract class RequestFunction(
topic
)
}

/**
* Return [TopicSender] for a given schema if that schema exists. This lets us wrap the data needed by
* processRequest without making changes to the method
* @param schemaName the name or path of the schema
* @param format the message format that the schema supports
* @return TopicSender if schema exists, null otherwise
* @throws InvalidParameterException if [schemaName] or [formatName] is not valid
*/
@Throws(InvalidParameterException::class)
internal fun getDummySender(schemaName: String?, formatName: String?): TopicSender {
val errMsgPrefix = "No client found in header so expected valid " +
"'$SCHEMA_PARAMETER' and '$FORMAT_PARAMETER' query parameters but found error: "
if (schemaName != null && formatName != null) {
val schema = workflowEngine.metadata.findSchema(schemaName)
?: throw InvalidParameterException("$errMsgPrefix The schema with name '$schemaName' does not exist")
val format = try {
Sender.Format.valueOf(formatName)
} catch (e: IllegalArgumentException) {
throw InvalidParameterException("$errMsgPrefix The format '$formatName' is not supported")
}
return TopicSender(
"ValidationSender",
"Internal",
format,
CustomerStatus.TESTING,
schemaName,
schema.topic
)
} else {
throw InvalidParameterException("$errMsgPrefix 'SchemaName' and 'format' parameters must not be null")
}
}
}
42 changes: 19 additions & 23 deletions prime-router/src/main/kotlin/azure/ValidateFunction.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,8 @@ import gov.cdc.prime.router.history.DetailedActionLog
import gov.cdc.prime.router.history.DetailedReport
import gov.cdc.prime.router.history.DetailedSubmissionHistory
import gov.cdc.prime.router.history.ReportStreamFilterResultForResponse
import gov.cdc.prime.router.tokens.AuthenticatedClaims
import gov.cdc.prime.router.tokens.authenticationFailure
import gov.cdc.prime.router.tokens.authorizationFailure
import org.apache.logging.log4j.kotlin.Logging
import java.security.InvalidParameterException
import java.time.OffsetDateTime

/**
Expand All @@ -44,30 +42,30 @@ class ValidateFunction(
*/
@FunctionName("validate")
@StorageAccount("AzureWebJobsStorage")
fun run(
fun validate(
@HttpTrigger(
name = "validate",
methods = [HttpMethod.POST],
authLevel = AuthorizationLevel.ANONYMOUS
) request: HttpRequestMessage<String?>
): HttpResponseMessage {
val senderName = extractClient(request)
if (senderName.isBlank()) {
return HttpUtilities.bad(request, "Expected a '$CLIENT_PARAMETER' query parameter")
}
return try {
val claims = AuthenticatedClaims.authenticate(request)
?: return HttpUtilities.unauthorizedResponse(request, authenticationFailure)

// Sender should eventually be obtained directly from who is authenticated
val sender = workflowEngine.settings.findSender(senderName)
?: return HttpUtilities.bad(request, "'$CLIENT_PARAMETER:$senderName': unknown sender")

if (!claims.authorizedForSendOrReceive(sender, request)) {
return HttpUtilities.unauthorizedResponse(request, authorizationFailure)
val sender: Sender?
val senderName = extractClient(request)
sender = if (senderName.isNotBlank()) {
workflowEngine.settings.findSender(senderName)
?: return HttpUtilities.bad(request, "'$CLIENT_PARAMETER:$senderName': unknown sender")
} else {
try {
getDummySender(
request.queryParameters.getOrDefault(SCHEMA_PARAMETER, null),
request.queryParameters.getOrDefault(FORMAT_PARAMETER, null)
)
} catch (e: InvalidParameterException) {
return HttpUtilities.bad(request, e.message.toString())
}
}
actionHistory.trackActionParams(request)

processRequest(request, sender)
} catch (ex: Exception) {
if (ex.message != null) {
Expand All @@ -80,7 +78,7 @@ class ValidateFunction(
}

/**
* Handles an incoming validation request after it has been authenticated via the /validate endpoint.
* Handles an incoming validation request from the /validate endpoint.
* @param request The incoming request
* @param sender The sender record, pulled from the database based on sender name on the request
* @return Returns an HttpResponseMessage indicating the result of the operation and any resulting information
Expand All @@ -96,9 +94,8 @@ class ValidateFunction(
try {
val validatedRequest = validateRequest(request)

// if the override parameter is populated, use that, otherwise use the sender value
val allowDuplicates = if
(!allowDuplicatesParam.isNullOrEmpty()) allowDuplicatesParam == "true"
// if the override parameter is populated, use that, otherwise use the sender value. Default to false.
val allowDuplicates = if (!allowDuplicatesParam.isNullOrEmpty()) allowDuplicatesParam == "true"
else {
sender.allowDuplicates
}
Expand All @@ -111,7 +108,6 @@ class ValidateFunction(
validatedRequest.routeTo,
allowDuplicates,
)

// return OK status, report validation was successful
HttpStatus.OK
} catch (e: ActionError) {
Expand Down
Loading

0 comments on commit b62c83d

Please sign in to comment.