diff --git a/prime-router/src/main/kotlin/cli/tests/AuthTests.kt b/prime-router/src/main/kotlin/cli/tests/AuthTests.kt index d679e57fbfd..8a74452beee 100644 --- a/prime-router/src/main/kotlin/cli/tests/AuthTests.kt +++ b/prime-router/src/main/kotlin/cli/tests/AuthTests.kt @@ -26,11 +26,9 @@ import gov.cdc.prime.router.tokens.AuthUtils import gov.cdc.prime.router.tokens.DatabaseJtiCache import gov.cdc.prime.router.tokens.Scope import io.ktor.client.plugins.timeout -import io.ktor.client.request.accept import io.ktor.client.request.get import io.ktor.http.ContentType import io.ktor.http.HttpStatusCode -import kotlinx.coroutines.runBlocking import java.io.File import java.io.IOException import java.net.URLEncoder @@ -1172,107 +1170,84 @@ class Server2ServerAuthTests : CoolTest() { ) val orgEndpoint = "${environment.url}/api/settings/organizations" - val client = HttpClientUtils.createDefaultHttpClient( - userToken - ) - - val clientAdmin = HttpClientUtils.createDefaultHttpClient( - adminToken - ) - // Case: GET All Org Settings (Admin-only endpoint) // Unhappy Path: user on admin-only endpoint - val response = runBlocking { - client.get(orgEndpoint) { - timeout { - requestTimeoutMillis = 45000 - // default timeout is 15s; raising higher due to slow Function startup issues - } - accept(ContentType.Application.Json) - } - } + val response = HttpClientUtils.get( + url = orgEndpoint, + accessToken = userToken, + timeout = 45000, // default timeout is 15s; raising higher due to slow Function startup issues + acceptedContent = ContentType.Application.Json + ) if (response.status != HttpStatusCode.Unauthorized) { bad( "***$name Test settings/organizations Unhappy Path (user-GET All Orgs) FAILED:" + - " Expected HttpStatus ${HttpStatusCode.Unauthorized}. Got ${response.status.value}" + " Expected HttpStatus ${HttpStatusCode.Unauthorized}. Got ${response.status.value}" ) return false } // Happy Path: admin on admin-only endpoint - val response2 = runBlocking { - clientAdmin.get(orgEndpoint) { - timeout { - requestTimeoutMillis = 45000 - // default timeout is 15s; raising higher due to slow Function startup issues - } - accept(ContentType.Application.Json) - } - } + val response2 = HttpClientUtils.get( + url = orgEndpoint, + accessToken = adminToken, + timeout = 45000, // default timeout is 15s; raising higher due to slow Function startup issues + acceptedContent = ContentType.Application.Json + ) if (response2.status != HttpStatusCode.OK) { bad( "***$name Test settings/organizations Happy Path (admin-GET All Orgs) FAILED:" + - " Expected HttpStatus ${HttpStatusCode.OK}. Got ${response2.status.value}" + " Expected HttpStatus ${HttpStatusCode.OK}. Got ${response2.status.value}" ) return false } // Case: GET Receivers for an Org (Endpoint allowed for admins and members of the org) // Happy Path: user on user-allowed endpoint - val response3 = runBlocking { - client.get("$orgEndpoint/${authorizedOrg.name}/receivers") { - timeout { - requestTimeoutMillis = 45000 - // default timeout is 15s; raising higher due to slow Function startup issues - } - accept(ContentType.Application.Json) - } - } + val response3 = HttpClientUtils.get( + url = "$orgEndpoint/${authorizedOrg.name}/receivers", + accessToken = userToken, + timeout = 45000, // default timeout is 15s; raising higher due to slow Function startup issues + acceptedContent = ContentType.Application.Json + ) if (response3.status != HttpStatusCode.OK) { bad( "***$name Test settings/organizations Happy Path (user-GET Org Receivers) FAILED:" + - " Expected HttpStatus ${HttpStatusCode.OK}. Got ${response3.status.value}" + " Expected HttpStatus ${HttpStatusCode.OK}. Got ${response3.status.value}" ) return false } // Happy Path: admin on user-allowed endpoint - val response4 = runBlocking { - clientAdmin.get("$orgEndpoint/${authorizedOrg.name}/receivers") { - timeout { - requestTimeoutMillis = 45000 - // default timeout is 15s; raising higher due to slow Function startup issues - } - accept(ContentType.Application.Json) - } - } + val response4 = HttpClientUtils.get( + url = "$orgEndpoint/${authorizedOrg.name}/receivers", + accessToken = adminToken, + timeout = 45000, // default timeout is 15s; raising higher due to slow Function startup issues + acceptedContent = ContentType.Application.Json + ) if (response4.status != HttpStatusCode.OK) { bad( "***$name Test settings/organizations Happy Path (admin-GET Org Receivers) FAILED:" + - " Expected HttpStatus ${HttpStatusCode.OK}. Got ${response4.status.value}" + " Expected HttpStatus ${HttpStatusCode.OK}. Got ${response4.status.value}" ) return false } // UnhappyPath: user on an unauthorized org name - val response5 = runBlocking { - client.get("$orgEndpoint/${unauthorizedOrg.name}/receivers") { - timeout { - requestTimeoutMillis = 45000 - // default timeout is 15s; raising higher due to slow Function startup issues - } - accept(ContentType.Application.Json) - } - } + val response5 = HttpClientUtils.get( + url = "$orgEndpoint/${unauthorizedOrg.name}/receivers", + accessToken = userToken, + timeout = 45000, // default timeout is 15s; raising higher due to slow Function startup issues + acceptedContent = ContentType.Application.Json + ) if (response5.status != HttpStatusCode.Unauthorized) { bad( "***$name Test settings/organizations Unhappy Path (user-GET Unauthorized Org Receivers) FAILED:" + - " Expected HttpStatus ${HttpStatusCode.Unauthorized}. Got ${response5.status.value}" + " Expected HttpStatus ${HttpStatusCode.Unauthorized}. Got ${response5.status.value}" ) return false } diff --git a/prime-router/src/main/kotlin/common/HttpClientUtils.kt b/prime-router/src/main/kotlin/common/HttpClientUtils.kt index 4e80e8f64f2..b406ef0bcb3 100644 --- a/prime-router/src/main/kotlin/common/HttpClientUtils.kt +++ b/prime-router/src/main/kotlin/common/HttpClientUtils.kt @@ -5,11 +5,9 @@ import io.ktor.client.call.body import io.ktor.client.engine.apache.Apache import io.ktor.client.plugins.HttpTimeout import io.ktor.client.plugins.contentnegotiation.ContentNegotiation -import io.ktor.client.plugins.defaultRequest import io.ktor.client.plugins.timeout import io.ktor.client.request.accept import io.ktor.client.request.forms.submitForm -import io.ktor.client.request.header import io.ktor.client.request.headers import io.ktor.client.request.parameter import io.ktor.client.request.request @@ -32,10 +30,30 @@ class HttpClientUtils { const val REQUEST_TIMEOUT_MILLIS: Long = 130000 // need to be public to be used by inline const val SETTINGS_REQUEST_TIMEOUT_MILLIS = 30000 + private val httpClient: HttpClient = + HttpClient(Apache) { + install(ContentNegotiation) { + json( + Json { + prettyPrint = true + isLenient = true + ignoreUnknownKeys = true + } + ) + } + install(HttpTimeout) + engine { + followRedirects = true + socketTimeout = TIMEOUT + connectTimeout = TIMEOUT + connectionRequestTimeout = TIMEOUT + } + } + /** * GET (query resource) operation to the given endpoint resource [url] * @param url: required, the url to the resource endpoint - * @param tokens: null default, the access token needed to call the endpoint + * @param accessToken: null default, the access token needed to call the endpoint * @param headers: null default, the headers of the request * @param acceptedContent: default application/json the accepted content type * @param timeout: default to a system base value in millis @@ -73,7 +91,7 @@ class HttpClientUtils { /** * GET (query resource) operation to the given endpoint resource [url] * @param url: required, the url to the resource endpoint - * @param tokens: null default, the access token needed to call the endpoint + * @param accessToken: null default, the access token needed to call the endpoint * @param headers: null default, the headers of the request * @param acceptedContent: default application/json the accepted content type * @param timeout: default to a system base value in millis @@ -106,7 +124,7 @@ class HttpClientUtils { /** * PUT (modify resource) operation to the given endpoint resource [url] * @param url: required, the url to the resource endpoint - * @param tokens: null default, the access token needed to call the endpoint + * @param accessToken: null default, the access token needed to call the endpoint * @param headers: null default, the headers of the request * @param acceptedContent: default application/json the accepted content type * @param timeout: default to a system base value in millis @@ -147,7 +165,7 @@ class HttpClientUtils { /** * PUT (modify resource) operation to the given endpoint resource [url] * @param url: required, the url to the resource endpoint - * @param tokens: null default, the access token needed to call the endpoint + * @param accessToken: null default, the access token needed to call the endpoint * @param headers: null default, the headers of the request * @param acceptedContent: default application/json the accepted content type * @param timeout: default to a system base value in millis @@ -183,7 +201,7 @@ class HttpClientUtils { /** * POST (create resource) operation to the given endpoint resource [url] * @param url: required, the url to the resource endpoint - * @param tokens: null default, the access token needed to call the endpoint + * @param accessToken: null default, the access token needed to call the endpoint * @param headers: null default, the headers of the request * @param acceptedContent: default application/json the accepted content type * @param timeout: default to a system base value in millis @@ -223,7 +241,7 @@ class HttpClientUtils { /** * POST (create resource) operation to the given endpoint resource [url] * @param url: required, the url to the resource endpoint - * @param tokens: null default, the access token needed to call the endpoint + * @param accessToken: null default, the access token needed to call the endpoint * @param headers: null default, the headers of the request * @param acceptedContent: default application/json the accepted content type * @param timeout: default to a system base value in millis @@ -260,7 +278,7 @@ class HttpClientUtils { * Submit form to the endpoint as indicated by [url] * * @param url: required, the url to the resource endpoint - * @param tokens: null default, the access token needed to call the endpoint + * @param accessToken: null default, the access token needed to call the endpoint * @param headers: null default, the headers of the request * @param acceptedContent: default application/json the accepted content type * @param timeout: default to a system base value in millis @@ -294,7 +312,7 @@ class HttpClientUtils { * Submit form to the endpoint as indicated by [url] * * @param url: required, the url to the resource endpoint - * @param tokens: null default, the access token needed to call the endpoint + * @param accessToken: null default, the access token needed to call the endpoint * @param headers: null default, the headers of the request * @param acceptedContent: default application/json the accepted content type * @param timeout: default to a system base value in millis @@ -312,7 +330,7 @@ class HttpClientUtils { httpClient: HttpClient? = null, ): HttpResponse { return runBlocking { - (httpClient ?: createDefaultHttpClient(accessToken)).submitForm( + (httpClient ?: getDefaultHttpClient()).submitForm( url, formParameters = Parameters.build { formParams?.forEach { param -> @@ -331,7 +349,11 @@ class HttpClientUtils { } } } - + accessToken?.let { + headers { + append("Authorization", "Bearer $accessToken") + } + } accept(acceptedContent) } } @@ -340,7 +362,7 @@ class HttpClientUtils { /** * HEAD operation to the given endpoint resource [url] * @param url: required, the url to the resource endpoint - * @param tokens: null default, the access token needed to call the endpoint + * @param accessToken: null default, the access token needed to call the endpoint * @param headers: null default, the headers of the request * @param acceptedContent: default application/json the accepted content type * @param timeout: default to a system base value in millis @@ -376,9 +398,9 @@ class HttpClientUtils { /** * HEAD operation to the given endpoint resource [url] * @param url: required, the url to the resource endpoint - * @param tokens: null default, the access token needed to call the endpoint + * @param accessToken: null default, the access token needed to call the endpoint * @param headers: null default, the headers of the request - * @param acceptContent: default application/json the accepted content type + * @param acceptedContent: default application/json the accepted content type * @param timeout: default to a system base value in millis * @param queryParameters: null default, query parameters of the request * @param httpClient: null default, a http client injected by caller @@ -411,7 +433,7 @@ class HttpClientUtils { * A thin wrapper on top of the underlying 3rd party http client, e.g. ktor http client * with: * @param url: required, the url to the resource endpoint - * @param tokens: null default, the access token needed to call the endpoint + * @param accessToken: null default, the access token needed to call the endpoint * @param headers: null default, the headers of the request * @param acceptedContent: default application/json the accepted content type * @param timeout: default to a system base value in millis @@ -450,7 +472,7 @@ class HttpClientUtils { * A thin wrapper on top of the underlying 3rd party http client, e.g. ktor http client * with: * @param url: required, the url to the resource endpoint - * @param tokens: null default, the access token needed to call the endpoint + * @param accessToken: null default, the access token needed to call the endpoint * @param headers: null default, the headers of the request * @param acceptedContent: default application/json the accepted content type * @param timeout: default to a system base value in millis @@ -496,17 +518,16 @@ class HttpClientUtils { httpClient: HttpClient? = null, ): HttpResponse { return runBlocking { - (httpClient ?: createDefaultHttpClient(accessToken)).request(url) { + (httpClient ?: getDefaultHttpClient()).request(url) { this.method = method timeout { requestTimeoutMillis = timeout } url { queryParameters?.forEach { - parameter(it.key, it.value.toString()) + parameter(it.key, it.value) } } - headers?.let { headers { headers.forEach { @@ -514,6 +535,11 @@ class HttpClientUtils { } } } + accessToken?.let { + headers { + append("Authorization", "Bearer $accessToken") + } + } acceptedContent?.let { accept(acceptedContent) contentType(acceptedContent) @@ -526,48 +552,15 @@ class HttpClientUtils { } /** - * Create a http client with sensible default settings + * Get a http client with sensible default settings * note: most configuration parameters are overridable * e.g. expectSuccess default to false because most of the time * the caller wants to handle the whole range of response status - * @param bearerTokens null default, the access token needed to call the endpoint + * * @return a HttpClient with all sensible defaults */ - fun createDefaultHttpClient(accessToken: String?): HttpClient { - return HttpClient(Apache) { - // installs logging into the call to post to the server - // commented out - not to override underlying default logger settings - // enable to trace http client internals when needed - // install(Logging) { - // logger = Logger.SIMPLE - // level = LogLevel.INFO - // } - // not using Bearer Auth handler due to refresh token behavior - accessToken?.let { - defaultRequest { - header("Authorization", "Bearer $it") - } - } - install(ContentNegotiation) { - json( - Json { - prettyPrint = true - isLenient = true - ignoreUnknownKeys = true - } - ) - } - - install(HttpTimeout) - engine { - followRedirects = true - socketTimeout = TIMEOUT - connectTimeout = TIMEOUT - connectionRequestTimeout = TIMEOUT - customizeClient { - } - } - } + fun getDefaultHttpClient(): HttpClient { + return httpClient } } } \ No newline at end of file