diff --git a/.environment/gitleaks/gitleaks-config.toml b/.environment/gitleaks/gitleaks-config.toml index da0f618980d..883ce714aff 100644 --- a/.environment/gitleaks/gitleaks-config.toml +++ b/.environment/gitleaks/gitleaks-config.toml @@ -203,6 +203,7 @@ title = "PRIME ReportStream Gitleaks Configuration" 'authority\", \"extension\"', # FHIR extension URL also shows up in normal FHIR test data 'ApiKeyCredential\(\"flexion\"', 'authType: \"two-legged\"', + 'authType == "two-legged"', '\"apiKey\"', 'api-key\" to \"oracle123\"', 'Authorization-Type: \"username/password\"', diff --git a/.github/actions/build-backend/action.yml b/.github/actions/build-backend/action.yml index a2aa9e2200f..ebf83067faa 100644 --- a/.github/actions/build-backend/action.yml +++ b/.github/actions/build-backend/action.yml @@ -39,7 +39,7 @@ runs: distribution: "temurin" cache: "gradle" - - uses: gradle/actions/setup-gradle@16bf8bc8fe830fa669c3c9f914d3eb147c629707 + - uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 - name: Lint if: inputs.run-integration-tests == 'true' diff --git a/.github/actions/build-submissions/action.yml b/.github/actions/build-submissions/action.yml index 15904917752..e683a5ad28d 100644 --- a/.github/actions/build-submissions/action.yml +++ b/.github/actions/build-submissions/action.yml @@ -39,7 +39,7 @@ runs: distribution: "temurin" cache: "gradle" - - uses: gradle/actions/setup-gradle@16bf8bc8fe830fa669c3c9f914d3eb147c629707 + - uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 - name: Lint if: inputs.run-integration-tests == 'true' diff --git a/.github/workflows/frontend_chromatic_main.yml b/.github/workflows/frontend_chromatic_main.yml index b6d051404f6..bed0aecd257 100644 --- a/.github/workflows/frontend_chromatic_main.yml +++ b/.github/workflows/frontend_chromatic_main.yml @@ -32,7 +32,7 @@ jobs: run: yarn install --immutable - name: Run Chromatic - uses: chromaui/action@95f238da20415287a1a877fecec79290ad2a7e0c + uses: chromaui/action@6eca23b4399151ac2cfc17fa95190d807c7e9519 with: workingDir: frontend-react token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/frontend_ci.yml b/.github/workflows/frontend_ci.yml index 328ec171966..671d9167e3b 100644 --- a/.github/workflows/frontend_ci.yml +++ b/.github/workflows/frontend_ci.yml @@ -247,7 +247,7 @@ jobs: - name: Run Chromatic id: chromatic - uses: chromaui/action@95f238da20415287a1a877fecec79290ad2a7e0c + uses: chromaui/action@6eca23b4399151ac2cfc17fa95190d807c7e9519 with: workingDir: frontend-react token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/snyk.yml b/.github/workflows/snyk.yml index 064c19860e7..e9e9e58c9c5 100644 --- a/.github/workflows/snyk.yml +++ b/.github/workflows/snyk.yml @@ -40,7 +40,7 @@ jobs: java-version: "17" distribution: "temurin" cache: "gradle" - - uses: gradle/actions/setup-gradle@16bf8bc8fe830fa669c3c9f914d3eb147c629707 + - uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 - name: Snyk Monitor working-directory: ${{ matrix.folder }} run: snyk monitor --org=prime-reportstream diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index c6e54cb4ff9..140dc169ab0 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -52,7 +52,7 @@ jobs: - name: "Get changed files with yaml" id: changed-files-yaml - uses: tj-actions/changed-files@e9772d140489982e0e3704fea5ee93d536f1e275 + uses: tj-actions/changed-files@48d8f15b2aaa3d255ca5af3eba4870f807ce6b3c with: files_yaml: | frontend: @@ -70,7 +70,7 @@ jobs: - name: Gradle setup if: steps.changed-files-yaml.outputs.backend_any_changed == 'true' || steps.branch-name.outputs.is_default == 'true' - uses: gradle/actions/setup-gradle@16bf8bc8fe830fa669c3c9f914d3eb147c629707 + uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 - name: Spin up build containers if: steps.changed-files-yaml.outputs.backend_any_changed == 'true' || steps.branch-name.outputs.is_default == 'true' diff --git a/.github/workflows/validate_terraform.yml b/.github/workflows/validate_terraform.yml index 310178f7101..0f5e50e6a91 100644 --- a/.github/workflows/validate_terraform.yml +++ b/.github/workflows/validate_terraform.yml @@ -48,7 +48,7 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 - name: Run Checkov action - uses: bridgecrewio/checkov-action@08a0f072354cdd9c009ce7c5c6174491834cec08 + uses: bridgecrewio/checkov-action@15c964c5bee933376cc576908ccfad6687718c8e with: directory: operations/app/terraform skip_check: CKV_AZURE_139,CKV_AZURE_137,CKV_AZURE_103,CKV_AZURE_104,CKV_AZURE_102,CKV_AZURE_130,CKV_AZURE_121,CKV_AZURE_67,CKV_AZURE_56,CKV_AZURE_17,CKV_AZURE_63,CKV_AZURE_18,CKV_AZURE_88,CKV_AZURE_65,CKV_AZURE_13,CKV_AZURE_66,CKV_AZURE_33,CKV_AZURE_35,CKV_AZURE_36,CKV_AZURE_98,CKV2_AZURE_1,CKV2_AZURE_15,CKV2_AZURE_21,CKV_AZURE_213,CKV_AZURE_59,CKV2_AZURE_33,CKV2_AZURE_32,CKV2_AZURE_28,CKV_AZURE_206,CKV_AZURE_42,CKV_AZURE_110,CKV_AZURE_109,CKV_AZURE_166,CKV2_AZURE_38,CKV2_AZURE_40,CKV2_AZURE_41,CKV_AZURE_235 diff --git a/frontend-react/e2e/pages/authenticated/organization.ts b/frontend-react/e2e/pages/authenticated/organization.ts index 6d24305989d..80a09aa4807 100644 --- a/frontend-react/e2e/pages/authenticated/organization.ts +++ b/frontend-react/e2e/pages/authenticated/organization.ts @@ -1,3 +1,4 @@ +import { expect } from "@playwright/test"; import { RSOrganizationSettings } from "../../../src/config/endpoints/settings"; import { MOCK_GET_ORGANIZATION_SETTINGS_LIST } from "../../mocks/organizations"; import { BasePage, BasePageTestArgs, type RouteHandlerFulfillEntry } from "../BasePage"; @@ -5,6 +6,7 @@ import { BasePage, BasePageTestArgs, type RouteHandlerFulfillEntry } from "../Ba export class OrganizationPage extends BasePage { static readonly API_ORGANIZATIONS = "/api/settings/organizations"; protected _organizationSettings: RSOrganizationSettings[]; + constructor(testArgs: BasePageTestArgs) { super( { @@ -38,4 +40,14 @@ export class OrganizationPage extends BasePage { }, ]; } + + async testTableHeaders() { + await expect(this.page.locator(".usa-table th").nth(0)).toHaveText(/Name/); + await expect(this.page.locator(".usa-table th").nth(1)).toHaveText(/Description/); + await expect(this.page.locator(".usa-table th").nth(2)).toHaveText(/Jurisdiction/); + await expect(this.page.locator(".usa-table th").nth(3)).toHaveText(/State/); + await expect(this.page.locator(".usa-table th").nth(4)).toHaveText(/County/); + + return true; + } } diff --git a/frontend-react/e2e/spec/all/authenticated/admin/organization-settings-page.spec.ts b/frontend-react/e2e/spec/all/authenticated/admin/organization-settings-page.spec.ts index ee9773e664c..ea405a97a83 100644 --- a/frontend-react/e2e/spec/all/authenticated/admin/organization-settings-page.spec.ts +++ b/frontend-react/e2e/spec/all/authenticated/admin/organization-settings-page.spec.ts @@ -1,10 +1,10 @@ -import {expect} from "@playwright/test"; -import {readFileSync} from "node:fs"; -import {join} from "node:path"; -import {fileURLToPath} from "node:url"; -import {MOCK_GET_ORGANIZATION_SETTINGS_LIST} from "../../../../mocks/organizations"; -import {OrganizationPage} from "../../../../pages/authenticated/organization"; -import {test as baseTest} from "../../../../test"; +import { expect } from "@playwright/test"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { MOCK_GET_ORGANIZATION_SETTINGS_LIST } from "../../../../mocks/organizations"; +import { OrganizationPage } from "../../../../pages/authenticated/organization"; +import { test as baseTest } from "../../../../test"; const __dirname = fileURLToPath(import.meta.url); diff --git a/frontend-react/e2e/spec/chromium-only/authenticated/daily-data-page-user-flow.spec.ts b/frontend-react/e2e/spec/chromium-only/authenticated/daily-data-page-user-flow.spec.ts index c39aaff902e..1a7001cadde 100644 --- a/frontend-react/e2e/spec/chromium-only/authenticated/daily-data-page-user-flow.spec.ts +++ b/frontend-react/e2e/spec/chromium-only/authenticated/daily-data-page-user-flow.spec.ts @@ -74,8 +74,7 @@ const SMOKE_RECEIVERS = [TEST_ORG_UP_RECEIVER_UP, TEST_ORG_CP_RECEIVER_CP, TEST_ test.describe( "Daily Data page - user flow smoke tests", { - // TODO: Investigate Daily Data page - user flow smoke tests › admin user › ignore org - FULL_ELR receiver › filter › on 'Apply' › clears 'Report ID' - //tag: "@smoke", + tag: "@smoke", }, () => { test.describe("admin user", () => { @@ -239,7 +238,6 @@ test.describe( filterStatusText = filterStatus([ TEST_ORG_UP_RECEIVER_UP, `${format(fromDate, "MM/dd/yyyy")}–${format(toDate, "MM/dd/yyyy")}`, - `${defaultStartTime}–${defaultEndTime}`, ]); await expect(dailyDataPage.page.getByTestId("filter-status")).toContainText( filterStatusText, @@ -340,7 +338,6 @@ test.describe( let filterStatusText = filterStatus([ TEST_ORG_UP_RECEIVER_UP, `${format(fromDate, "MM/dd/yyyy")}–${format(toDate, "MM/dd/yyyy")}`, - `${defaultStartTime}–${defaultEndTime}`, ]); await expect(dailyDataPage.page.getByTestId("filter-status")).toContainText(filterStatusText); diff --git a/frontend-react/e2e/spec/chromium-only/authenticated/organization-settings-page-user-flow.spec.ts b/frontend-react/e2e/spec/chromium-only/authenticated/organization-settings-page-user-flow.spec.ts new file mode 100644 index 00000000000..fa74fc13713 --- /dev/null +++ b/frontend-react/e2e/spec/chromium-only/authenticated/organization-settings-page-user-flow.spec.ts @@ -0,0 +1,118 @@ +import { expect } from "@playwright/test"; +import { tableRows } from "../../../helpers/utils"; +import { MOCK_GET_ORGANIZATION_SETTINGS_LIST } from "../../../mocks/organizations"; +import { OrganizationPage } from "../../../pages/authenticated/organization"; +import { test as baseTest } from "../../../test"; + + +export interface OrganizationPageFixtures { + organizationPage: OrganizationPage; +} + +const test = baseTest.extend({ + organizationPage: async ( + { + page: _page, + isMockDisabled, + adminLogin, + senderLogin, + receiverLogin, + storageState, + frontendWarningsLogPath, + isFrontendWarningsLog, + }, + use, + ) => { + const page = new OrganizationPage({ + page: _page, + isMockDisabled, + adminLogin, + senderLogin, + receiverLogin, + storageState, + frontendWarningsLogPath, + isFrontendWarningsLog, + }); + await page.goto(); + await use(page); + }, +}); + +test.describe("Admin Organization Settings Page - user flow smoke tests", { + tag: "@smoke", +}, () => { + test.describe("admin user", () => { + test.use({storageState: "e2e/.auth/admin.json"}); + + test.describe("header", () => { + test("has correct title + heading", async ({organizationPage}) => { + await organizationPage.testHeader(); + }); + }); + + test.describe("table", () => { + test.beforeEach(async ({organizationPage}) => { + await organizationPage.page.locator(".usa-table tbody").waitFor({state: "visible"}); + }); + + test("has correct headers", async ({organizationPage}) => { + const result = await organizationPage.testTableHeaders(); + expect(result).toBe(true); + }); + + test("displays data", async ({organizationPage}) => { + const rowCount = await tableRows(organizationPage.page).count(); + // Heading with result length + await expect( + organizationPage.page.getByRole("heading", { + name: `Organizations (${rowCount})`, + }), + ).toBeVisible(); + }); + + test("filtering works as expected", async ({organizationPage}) => { + const table = organizationPage.page.getByRole("table"); + const {description, name, jurisdiction, stateCode} = MOCK_GET_ORGANIZATION_SETTINGS_LIST[2]; + const filterBox = organizationPage.page.getByRole("textbox", { + name: "Filter:", + }); + + await expect(filterBox).toBeVisible(); + + await filterBox.fill(name); + const rows = await table.getByRole("row").all(); + expect(rows).toHaveLength(2); + const cols = rows[1].getByRole("cell").allTextContents(); + const expectedColContents = [ + name, + description ?? "", + jurisdiction ?? "", + stateCode ?? "", + "", + "SetEdit", + ]; + + for (const [i, col] of (await cols).entries()) { + expect(col).toBe(expectedColContents[i]); + } + }); + + test('selecting "Set" updates link label in navigation', async ({organizationPage}) => { + const firstDataRow = organizationPage.page.getByRole("table").getByRole("row").nth(1); + const firstDataRowName = (await firstDataRow.getByRole("cell").nth(0).textContent()) ?? "INVALID"; + const setButton = firstDataRow.getByRole("button", { + name: "Set", + }); + + await expect(setButton).toBeVisible(); + await setButton.click(); + + const orgLink = organizationPage.page.getByRole("link", { + name: firstDataRowName, + }); + await expect(orgLink).toBeVisible(); + await expect(orgLink).toHaveAttribute("href", "/admin/settings"); + }); + }); + }); +}); diff --git a/prime-router/settings/STLTs/MN/mn-doh.yml b/prime-router/settings/STLTs/MN/mn-doh.yml old mode 100644 new mode 100755 index 46ac4a93244..616c21562d6 --- a/prime-router/settings/STLTs/MN/mn-doh.yml +++ b/prime-router/settings/STLTs/MN/mn-doh.yml @@ -76,8 +76,8 @@ processingModeFilter: [] reverseTheQualityFilter: false conditionFilter: - # RSV: 55735004 (+), COVID: 840539006 (+), FLU (A or B by Antigen): 6142004 (+), FLU (A or B by Culture and Identification Method),: 541131000124102, They also want MPOX but, we are not ready to send MPOX yet. - - "%resource.where(interpretation.coding.code = 'A').code.coding.extension('https://reportstream.cdc.gov/fhir/StructureDefinition/condition-code').value.where(code in ('55735004'|'840539006'|'6142004'|'541131000124102')).exists()" + # RSV: 55735004 (+), COVID: 840539006 (+), FLU (A or B by Antigen): 541131000124102, They also want MPOX but, we are not ready to send MPOX yet. + - "%resource.where(interpretation.coding.code = 'A').code.coding.extension('https://reportstream.cdc.gov/fhir/StructureDefinition/condition-code').value.where(code in ('55735004'|'840539006')).exists()" mappedConditionFilter: [] deidentify: false deidentifiedValue: "" diff --git a/prime-router/src/main/kotlin/Report.kt b/prime-router/src/main/kotlin/Report.kt index 73fe5cc48bd..9d3c7c91adb 100644 --- a/prime-router/src/main/kotlin/Report.kt +++ b/prime-router/src/main/kotlin/Report.kt @@ -134,8 +134,8 @@ data class ReportStreamFilterResult( val filteredTrackingElement: String, val filterType: ReportStreamFilterType?, val filteredObservationDetails: String? = null, + override val scope: ActionLogScope = ActionLogScope.translation, ) : ActionLogDetail { - override val scope = ActionLogScope.translation override val errorCode = ErrorCode.UNKNOWN companion object { @@ -145,8 +145,8 @@ data class ReportStreamFilterResult( override val message = """ For $receiverName, filter $filterName$filterArgs filtered out item $filteredTrackingElement. - $filteredObservationDetails - } + Filter Type: $filterType Filter Args: $filterArgs + Filtered Observation Details: $filteredObservationDetails """.trimIndent() // Used for deserializing to a JSON response diff --git a/prime-router/src/main/kotlin/azure/CheckFunction.kt b/prime-router/src/main/kotlin/azure/CheckFunction.kt index 740478ef8f4..9b11303c524 100644 --- a/prime-router/src/main/kotlin/azure/CheckFunction.kt +++ b/prime-router/src/main/kotlin/azure/CheckFunction.kt @@ -19,7 +19,13 @@ import gov.cdc.prime.router.common.JacksonMapperUtilities import gov.cdc.prime.router.tokens.AuthenticatedClaims import gov.cdc.prime.router.tokens.authenticationFailure import gov.cdc.prime.router.transport.RESTTransport +import gov.cdc.prime.router.transport.RESTTransport.Companion.buildHeaders import gov.cdc.prime.router.transport.SftpTransport +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.get +import io.ktor.client.statement.HttpResponse +import io.ktor.http.HttpStatusCode import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import net.schmizz.sshj.sftp.RemoteResourceFilter @@ -342,6 +348,8 @@ class CheckFunction : Logging { ): Boolean { logger.info("REST Transport $restTransportType") responseBody.add("${receiver.fullName}: REST Transport") + var msg = "${receiver.fullName}: Success: received connection is alive" + var retVal = true try { val theRESTTransport = RESTTransport() val reportId = UUID.randomUUID().toString() @@ -354,28 +362,65 @@ class CheckFunction : Logging { runBlocking { launch { val httpHeaders = theRESTTransport.getHeaders(restTransportType, reportId) - val accessToken: String = - theRESTTransport.getOAuthToken( - restTransportType, - jksCredential, - credential, - aLogger + + val accessToken = theRESTTransport.getAccessToken( + restTransportType, + jksCredential, + credential, + httpHeaders, + aLogger + ) + + // Try to GET something from the endpoint + val response = getFromUrl( + restTransportType.reportUrl, + httpHeaders, + RESTTransport.createDefaultHttpClient( + jksCredential, accessToken, + restTransportType ) + ) - val msg = when { - accessToken.isNotEmpty() -> "${receiver.fullName}: Success: received OAuth token" - httpHeaders.isNotEmpty() -> "${receiver.fullName}: Success: received Authentication header" - else -> error("${receiver.fullName}: Failure: no valid response from RESTTransport") + if (response.status == HttpStatusCode.InternalServerError) { + msg = "${receiver.fullName}: Failure: 500 Internal Error Occured" + retVal = false } - logger.info(msg) - responseBody.add(msg) } } } catch (t: Throwable) { - trackException(t, responseBody, receiver) - return false + if (t.message!!.contains("connect_timeout") || + t.message!!.contains("Unable to find credentials") + ) { + // Fail if there is timeout or Unable to find credential from Vault + trackException(t, responseBody, receiver) + return false + } + } + + responseBody.add(msg) + return retVal + } + + /** + * getFromURL extracts something from provided URL. + * + * @param url - Url to extract + * @param headers - headers + * @param httpClient - given http client engine + * + */ + suspend fun getFromUrl( + url: String, + headers: Map, + httpClient: HttpClient, + ): HttpResponse { + httpClient.use { client -> + return client.get(url) { + buildHeaders( + headers.map { (key, value) -> Pair(key, value) }.toMap() + ) + } } - return true } /** diff --git a/prime-router/src/main/kotlin/fhirengine/engine/FHIRReceiverFilter.kt b/prime-router/src/main/kotlin/fhirengine/engine/FHIRReceiverFilter.kt index cc732026df9..c73f92d0206 100644 --- a/prime-router/src/main/kotlin/fhirengine/engine/FHIRReceiverFilter.kt +++ b/prime-router/src/main/kotlin/fhirengine/engine/FHIRReceiverFilter.kt @@ -13,6 +13,7 @@ import gov.cdc.prime.router.MimeFormat import gov.cdc.prime.router.Options import gov.cdc.prime.router.Receiver import gov.cdc.prime.router.Report +import gov.cdc.prime.router.ReportStreamFilterResult import gov.cdc.prime.router.ReportStreamFilterType import gov.cdc.prime.router.SettingsProvider import gov.cdc.prime.router.azure.ActionHistory @@ -29,6 +30,7 @@ import gov.cdc.prime.router.azure.observability.context.MDCUtils import gov.cdc.prime.router.azure.observability.context.withLoggingContext import gov.cdc.prime.router.azure.observability.event.AzureEventService import gov.cdc.prime.router.azure.observability.event.AzureEventServiceImpl +import gov.cdc.prime.router.azure.observability.event.AzureEventUtils import gov.cdc.prime.router.azure.observability.event.ReportStreamEventName import gov.cdc.prime.router.azure.observability.event.ReportStreamEventProperties import gov.cdc.prime.router.codes @@ -335,6 +337,7 @@ class FHIRReceiverFilter( // track input report actionHistory.trackExistingInputReport(queueMessage.reportId) + actionLogger.setReportId(queueMessage.reportId) // gather receiver and sender objects val receiver = settings.receivers.first { it.fullName == queueMessage.receiverFullName } @@ -416,7 +419,8 @@ class FHIRReceiverFilter( emptyList(), 1, metadata = this.metadata, - topic = queueMessage.topic + topic = queueMessage.topic, + destination = receiver ) // create item lineage @@ -434,6 +438,19 @@ class FHIRReceiverFilter( ) ) + // add filter results + emptyReport.filteringResults.add( + ReportStreamFilterResult( + receiver.fullName, + db.fetchReportFile(queueMessage.reportId).itemCount, + filterResult.failingFilter.filters.joinToString("\n"), + emptyList(), + AzureEventUtils.getIdentifier(bundle).value ?: "", + filterResult.failingFilter.filterType, + scope = ActionLogScope.report + ) + ) + // ensure tracking is set actionHistory.trackCreatedReport(nextEvent, emptyReport) diff --git a/prime-router/src/main/kotlin/transport/RESTTransport.kt b/prime-router/src/main/kotlin/transport/RESTTransport.kt index 7f7f0c8e448..6b0758c9ccb 100644 --- a/prime-router/src/main/kotlin/transport/RESTTransport.kt +++ b/prime-router/src/main/kotlin/transport/RESTTransport.kt @@ -109,26 +109,14 @@ class RESTTransport(private val httpClient: HttpClient? = null) : ITransport { launch { try { val httpHeaders = getHeaders(restTransportInfo, reportId) - var accessToken: String? = null - - if (restTransportInfo.authType == "apiKey") { - val apiKeyCredential = credential as UserApiKeyCredential - httpHeaders["shared-api-key"] = apiKeyCredential.apiKey - httpHeaders["System_ID"] = apiKeyCredential.user - httpHeaders["Key"] = apiKeyCredential.apiKey - accessToken = apiKeyCredential.apiKey - } - if (restTransportInfo.authType == "two-legged" || restTransportInfo.authType == null) { - // parse headers for any dynamic values, OK needs the report ID - accessToken = getOAuthToken( - restTransportInfo, - jksCredential, - credential, - logger - ) - logger.info("Token successfully added!") - } + val accessToken = getAccessToken( + restTransportInfo, + jksCredential, + credential, + httpHeaders, + logger + ) // If encryption is needed. if (restTransportInfo.encryptionKeyUrl.isNotEmpty()) { @@ -322,6 +310,43 @@ class RESTTransport(private val httpClient: HttpClient? = null) : ITransport { }.toMutableMap() } + /** + * Get the Accesstoken based on authType given in Restransport header + * + * @param restTransportInfo - Transport setting + * @param jksCredential The jks credential + */ + suspend fun getAccessToken( + restTransportInfo: RESTTransportType, + jksCredential: UserJksCredential?, + credential: RestCredential, + httpHeaders: MutableMap, + logger: Logger, + ): String? { + var accessToken: String? = null + + if (restTransportInfo.authType == "apiKey") { + val apiKeyCredential = credential as UserApiKeyCredential + httpHeaders["shared-api-key"] = apiKeyCredential.apiKey + httpHeaders["System_ID"] = apiKeyCredential.user + httpHeaders["Key"] = apiKeyCredential.apiKey + accessToken = apiKeyCredential.apiKey + } + + if (restTransportInfo.authType == "two-legged" || restTransportInfo.authType == null) { + // parse headers for any dynamic values, OK needs the report ID + accessToken = getOAuthToken( + restTransportInfo, + jksCredential, + credential, + logger + ) + logger.info("Token successfully added!") + } + + return accessToken + } + /** * Get the OAuth token based on credential type * @@ -621,7 +646,7 @@ class RESTTransport(private val httpClient: HttpClient? = null) : ITransport { } /** Our default Http Client, with an optional SSL context, and optional auth token */ - private fun createDefaultHttpClient( + fun createDefaultHttpClient( jks: UserJksCredential?, accessToken: String?, restTransportInfo: RESTTransportType?, diff --git a/prime-router/src/test/kotlin/fhirengine/azure/FHIRReceiverFilterIntegrationTests.kt b/prime-router/src/test/kotlin/fhirengine/azure/FHIRReceiverFilterIntegrationTests.kt index bc73514cff2..7580e6d67bb 100644 --- a/prime-router/src/test/kotlin/fhirengine/azure/FHIRReceiverFilterIntegrationTests.kt +++ b/prime-router/src/test/kotlin/fhirengine/azure/FHIRReceiverFilterIntegrationTests.kt @@ -1,7 +1,6 @@ package gov.cdc.prime.router.fhirengine.azure import assertk.assertThat -import assertk.assertions.hasSameSizeAs import assertk.assertions.hasSize import assertk.assertions.isEqualTo import assertk.assertions.isEqualToIgnoringGivenProperties @@ -19,6 +18,7 @@ import gov.cdc.prime.router.DeepOrganization import gov.cdc.prime.router.FileSettings import gov.cdc.prime.router.Report import gov.cdc.prime.router.ReportStreamFilter +import gov.cdc.prime.router.ReportStreamFilterResult import gov.cdc.prime.router.ReportStreamFilterType import gov.cdc.prime.router.Sender import gov.cdc.prime.router.Topic @@ -839,9 +839,21 @@ class FHIRReceiverFilterIntegrationTests : Logging { .from(Tables.ACTION_LOG) .fetchInto(ActionLog::class.java) - assertThat(actionLogRecords).hasSameSizeAs(fullElrQualityFilterSample) + assertThat(actionLogRecords).hasSize(fullElrQualityFilterSample.size + 1) - actionLogRecords.forEachIndexed { index, actionLog -> + with(actionLogRecords.first()) { + assertThat(this.type).isEqualTo(ActionLogLevel.filter) + assertThat(this.scope).isEqualTo(ActionLogScope.report) + assertThat(this.trackingId).isEqualTo(validFHIRRecord1Identifier) + assertThat(this.detail).isInstanceOf() + .matchesPredicate { + it.filterName == fullElrQualityFilterSample.joinToString("\n") && + it.filterType == ReportStreamFilterType.QUALITY_FILTER && + it.receiverName == receiver.fullName + } + } + + actionLogRecords.slice(1.. assertThat(actionLog.trackingId).isEqualTo(validFHIRRecord1Identifier) assertThat(actionLog.detail).isInstanceOf() .matchesPredicate { @@ -1105,10 +1117,24 @@ class FHIRReceiverFilterIntegrationTests : Logging { .from(Tables.ACTION_LOG) .fetchInto(ActionLog::class.java) - assertThat(actionLogRecords).hasSize(1) + assertThat(actionLogRecords).hasSize(2) + + val expectedTrackingId = AzureEventUtils.getIdentifier(FhirTranscoder.decode(reportContents)).value!! + + with(actionLogRecords.first()) { + assertThat(this.type).isEqualTo(ActionLogLevel.filter) + assertThat(this.scope).isEqualTo(ActionLogScope.report) + assertThat(this.trackingId).isEqualTo(expectedTrackingId) + assertThat(this.detail).isInstanceOf() + .matchesPredicate { + it.filterName == processingModeFilterDebugging.single() && + it.filterType == ReportStreamFilterType.PROCESSING_MODE_FILTER && + it.receiverName == receiver.fullName + } + } - with(actionLogRecords.single()) { - assertThat(this.trackingId).isEqualTo("MT_COCNB_ORU_NBPHELR.1.5348467") + with(actionLogRecords.last()) { + assertThat(this.trackingId).isEqualTo(expectedTrackingId) assertThat(this.type).isEqualTo(ActionLogLevel.warning) assertThat(this.scope).isEqualTo(ActionLogScope.item) assertThat(this.index).isEqualTo(1) diff --git a/prime-router/src/test/kotlin/fhirengine/engine/FhirReceiverFilterTests.kt b/prime-router/src/test/kotlin/fhirengine/engine/FhirReceiverFilterTests.kt index 950f828eea0..d71ad140e10 100644 --- a/prime-router/src/test/kotlin/fhirengine/engine/FhirReceiverFilterTests.kt +++ b/prime-router/src/test/kotlin/fhirengine/engine/FhirReceiverFilterTests.kt @@ -17,6 +17,7 @@ import gov.cdc.prime.router.MimeFormat import gov.cdc.prime.router.Organization import gov.cdc.prime.router.Receiver import gov.cdc.prime.router.Report +import gov.cdc.prime.router.ReportId import gov.cdc.prime.router.ReportStreamConditionFilter import gov.cdc.prime.router.ReportStreamFilter import gov.cdc.prime.router.ReportStreamFilterType @@ -170,6 +171,7 @@ class FhirReceiverFilterTests { actionLogger = ActionLogger() actionHistory.reportsIn.clear() actionHistory.reportsOut.clear() + actionHistory.reportsReceived.clear() actionHistory.actionLogs.clear() azureEventService.clear() mockkObject(BlobAccess) @@ -203,6 +205,14 @@ class FhirReceiverFilterTests { mockkObject(BlobAccess) every { BlobAccess.uploadBlob(any(), any()) } returns "test" every { accessSpy.insertTask(any(), MimeFormat.FHIR.toString(), BODY_URL, any()) }.returns(Unit) + every { accessSpy.fetchReportFile(any()) } answers { + val reportId = firstArg() + if (reportId in messages.map { it.reportId }) { + ReportFile().setReportId(reportId).setItemCount(1) + } else { + callOriginal() + } + } // act on each message (with assert) messages.forEach { message -> @@ -256,6 +266,14 @@ class FhirReceiverFilterTests { mockkObject(BlobAccess) every { BlobAccess.uploadBlob(any(), any()) } returns "test" every { accessSpy.insertTask(any(), MimeFormat.FHIR.toString(), BODY_URL, any()) }.returns(Unit) + every { accessSpy.fetchReportFile(any()) } answers { + val reportId = firstArg() + if (reportId in messages.map { it.reportId }) { + ReportFile().setReportId(reportId).setItemCount(1) + } else { + callOriginal() + } + } // act messages.forEach { message -> @@ -308,6 +326,14 @@ class FhirReceiverFilterTests { mockkObject(BlobAccess) every { BlobAccess.uploadBlob(any(), any()) } returns "test" every { accessSpy.insertTask(any(), MimeFormat.FHIR.toString(), BODY_URL, any()) }.returns(Unit) + every { accessSpy.fetchReportFile(any()) } answers { + val reportId = firstArg() + if (reportId in messages.map { it.reportId }) { + ReportFile().setReportId(reportId).setItemCount(1) + } else { + callOriginal() + } + } // act on each message (with assert) messages.forEach { message -> @@ -362,6 +388,14 @@ class FhirReceiverFilterTests { mockkObject(BlobAccess) every { BlobAccess.uploadBlob(any(), any()) } returns "test" every { accessSpy.insertTask(any(), MimeFormat.FHIR.toString(), BODY_URL, any()) }.returns(Unit) + every { accessSpy.fetchReportFile(any()) } answers { + val reportId = firstArg() + if (reportId in messages.map { it.reportId }) { + ReportFile().setReportId(reportId).setItemCount(1) + } else { + callOriginal() + } + } // act on each message (with assert) messages.forEach { message -> @@ -415,6 +449,14 @@ class FhirReceiverFilterTests { every { BlobAccess.downloadBlob(any(), any()) }.returns(File(VALID_FHIR_FILEPATH).readText()) every { BlobAccess.uploadBlob(any(), any()) } returns "test" every { accessSpy.insertTask(any(), MimeFormat.FHIR.toString(), BODY_URL, any()) }.returns(Unit) + every { accessSpy.fetchReportFile(any()) } answers { + val reportId = firstArg() + if (reportId == message.reportId) { + ReportFile().setReportId(reportId).setItemCount(1) + } else { + callOriginal() + } + } // act + assert accessSpy.transact { txn -> @@ -479,6 +521,14 @@ class FhirReceiverFilterTests { every { BlobAccess.downloadBlob(any(), any()) }.returns(FhirTranscoder.encode(bundle)) every { BlobAccess.uploadBlob(any(), any()) } returns "test" every { accessSpy.insertTask(any(), MimeFormat.FHIR.toString(), BODY_URL, any()) }.returns(Unit) + every { accessSpy.fetchReportFile(any()) } answers { + val reportId = firstArg() + if (reportId == message.reportId) { + ReportFile().setReportId(reportId).setItemCount(1) + } else { + callOriginal() + } + } // act + assert accessSpy.transact { _ -> @@ -733,6 +783,14 @@ class FhirReceiverFilterTests { every { BlobAccess.downloadBlob(any(), any()) }.returns(fhirData) every { BlobAccess.uploadBlob(any(), any()) } returns "test" every { accessSpy.insertTask(any(), MimeFormat.FHIR.toString(), BODY_URL, any()) }.returns(Unit) + every { accessSpy.fetchReportFile(any()) } answers { + val reportId = firstArg() + if (reportId == message.reportId) { + ReportFile().setReportId(reportId).setItemCount(1) + } else { + callOriginal() + } + } every { any().filterObservations(any(), any()) } returns FhirTranscoder.decode(fhirData) // act + assert @@ -797,6 +855,14 @@ class FhirReceiverFilterTests { // mock setup every { BlobAccess.downloadBlob(any(), any()) }.returns(FhirTranscoder.encode(bundle)) + every { accessSpy.fetchReportFile(any()) } answers { + val reportId = firstArg() + if (reportId == message.reportId) { + ReportFile().setReportId(reportId).setItemCount(1) + } else { + callOriginal() + } + } // act + assert accessSpy.transact { txn -> @@ -840,6 +906,14 @@ class FhirReceiverFilterTests { // mock setup every { BlobAccess.downloadBlob(any(), any()) }.returns(File(VALID_FHIR_FILEPATH).readText()) + every { accessSpy.fetchReportFile(any()) } answers { + val reportId = firstArg() + if (reportId == message.reportId) { + ReportFile().setReportId(reportId).setItemCount(1) + } else { + callOriginal() + } + } // act + assert accessSpy.transact { txn ->