diff --git a/CHANGELOG.md b/CHANGELOG.md index 7aed4757..1dc76890 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog ## [Unreleased] +### Added +- HTTP logging support for OAuth 2.0 Password Grant authentication, by @HardNorth +### Fixed +- Explicitly disable proxy for OAuth authentication when `rp.oauth.use.proxy=false` to avoid issues when proxy is set through system properties, by @HardNorth ## [5.4.6] ### Added diff --git a/build.gradle b/build.gradle index 5adc8420..a8aac7a1 100644 --- a/build.gradle +++ b/build.gradle @@ -61,7 +61,7 @@ dependencies { implementation "org.aspectj:aspectjweaver:${project.aspectj_version}" implementation "org.slf4j:slf4j-api:${slf4j_version}" - implementation 'org.apache.commons:commons-lang3:3.18.0' + implementation 'org.apache.commons:commons-lang3:3.19.0' testImplementation "org.slf4j:jul-to-slf4j:${slf4j_version}" testImplementation("org.junit.platform:junit-platform-runner:${project.junit_runner_version}") { @@ -80,6 +80,7 @@ dependencies { } testImplementation 'commons-io:commons-io:2.17.0' testImplementation 'com.epam.reportportal:agent-java-test-utils:0.1.0' + testImplementation "com.squareup.okhttp3:mockwebserver:${project.okhttp_version}" } test { diff --git a/src/main/java/com/epam/reportportal/listeners/ListenerParameters.java b/src/main/java/com/epam/reportportal/listeners/ListenerParameters.java index 848acf43..a698c9ab 100644 --- a/src/main/java/com/epam/reportportal/listeners/ListenerParameters.java +++ b/src/main/java/com/epam/reportportal/listeners/ListenerParameters.java @@ -197,6 +197,7 @@ public ListenerParameters() { this.convertImage = DEFAULT_CONVERT_IMAGE; this.reportingTimeout = DEFAULT_REPORTING_TIMEOUT; this.httpLogging = DEFAULT_HTTP_LOGGING; + this.oauthUseProxy = DEFAULT_OAUTH_USE_PROXY; this.keystoreType = DEFAULT_KEYSTORE_TYPE; this.truststoreType = DEFAULT_KEYSTORE_TYPE; diff --git a/src/main/java/com/epam/reportportal/service/BearerAuthInterceptor.java b/src/main/java/com/epam/reportportal/service/BearerAuthInterceptor.java index 81938e3e..d9618532 100644 --- a/src/main/java/com/epam/reportportal/service/BearerAuthInterceptor.java +++ b/src/main/java/com/epam/reportportal/service/BearerAuthInterceptor.java @@ -1,11 +1,11 @@ /* - * Copyright 2019 EPAM Systems + * Copyright 2025 EPAM Systems * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.epam.reportportal.service; import jakarta.annotation.Nonnull; @@ -27,16 +28,16 @@ */ public class BearerAuthInterceptor implements Interceptor { - private final String apiKey; + private final String authHeaderValue; public BearerAuthInterceptor(String apiKey) { - this.apiKey = apiKey; + this.authHeaderValue = "Bearer " + apiKey; } @Override @Nonnull public Response intercept(Chain chain) throws IOException { - Request rq = chain.request().newBuilder().addHeader("Authorization", "Bearer " + apiKey).build(); + Request rq = chain.request().newBuilder().addHeader("Authorization", authHeaderValue).build(); return chain.proceed(rq); } } diff --git a/src/main/java/com/epam/reportportal/service/OAuth2PasswordGrantAuthInterceptor.java b/src/main/java/com/epam/reportportal/service/OAuth2PasswordGrantAuthInterceptor.java index e15bee72..aa5c8d41 100644 --- a/src/main/java/com/epam/reportportal/service/OAuth2PasswordGrantAuthInterceptor.java +++ b/src/main/java/com/epam/reportportal/service/OAuth2PasswordGrantAuthInterceptor.java @@ -30,6 +30,7 @@ import java.io.IOException; import java.net.MalformedURLException; +import java.net.Proxy; import java.net.URL; import java.util.Map; import java.util.Objects; @@ -101,9 +102,13 @@ public OAuth2PasswordGrantAuthInterceptor(@Nonnull ListenerParameters parameters URL tokenUrl = parseTokenUri(parameters); OkHttpClient.Builder clientBuilder = ClientUtils.setupSsl(new OkHttpClient.Builder(), tokenUrl, parameters); + ClientUtils.setupHttpLoggingInterceptor(clientBuilder, parameters); if (parameters.isOauthUseProxy()) { ClientUtils.setupProxy(clientBuilder, parameters); + } else { + // Explicitly disable proxy to override system proxy settings + clientBuilder.proxy(Proxy.NO_PROXY); } ofNullable(parameters.getHttpConnectTimeout()).ifPresent(d -> clientBuilder.connectTimeout(d.toMillis(), TimeUnit.MILLISECONDS)); diff --git a/src/test/java/com/epam/reportportal/service/LaunchMicrosecondsTest.java b/src/test/java/com/epam/reportportal/service/LaunchMicrosecondsTest.java index 5340ab8b..964baf79 100644 --- a/src/test/java/com/epam/reportportal/service/LaunchMicrosecondsTest.java +++ b/src/test/java/com/epam/reportportal/service/LaunchMicrosecondsTest.java @@ -17,12 +17,14 @@ package com.epam.reportportal.service; import com.epam.reportportal.listeners.ListenerParameters; +import com.epam.reportportal.listeners.LogLevel; import com.epam.reportportal.test.TestUtils; import com.epam.reportportal.util.test.SocketUtils; import com.epam.ta.reportportal.ws.model.FinishExecutionRQ; import com.epam.ta.reportportal.ws.model.FinishTestItemRQ; import com.epam.ta.reportportal.ws.model.StartTestItemRQ; import com.epam.ta.reportportal.ws.model.launch.StartLaunchRQ; +import com.epam.ta.reportportal.ws.model.log.SaveLogRQ; import io.reactivex.Maybe; import org.apache.commons.lang3.tuple.Pair; import org.junit.jupiter.api.AfterEach; @@ -106,6 +108,14 @@ private static FinishExecutionRQ buildFinishLaunchRq(Comparable> logTime) { + SaveLogRQ rq = new SaveLogRQ(); + rq.setLogTime(logTime); + rq.setLevel(LogLevel.INFO.name()); + rq.setMessage("some message"); + return rq; + } + private static Comparable> dateOrInstant(boolean instant) { Instant testInstant = Instant.ofEpochSecond(START_TIME_SECONDS_BASE, START_TIME_NANO_ADJUSTMENT); if (!instant) { @@ -163,7 +173,7 @@ private static SocketUtils.ServerCallable buildServerCallableForStartItem(Server private static SocketUtils.ServerCallable buildServerCallableForFinishItem(ServerSocket ss, boolean micro) { List responses = new ArrayList<>(); - responses.add(micro ? "files/responses/info_response_microseconds.txt" : "files/responses/info_response_no_microseconds.txt"); + responses.add(micro ? "files/responses/info_response_microseconds.txt" : "files/responses/info_response_no_microseconds2.txt"); responses.add("files/responses/start_launch_response.txt"); responses.add("files/responses/simple_response.txt"); // finish item response return new SocketUtils.ServerCallable(ss, Collections.emptyMap(), responses); @@ -178,6 +188,14 @@ private static SocketUtils.ServerCallable buildServerCallableForFinishLaunch(Ser return new SocketUtils.ServerCallable(ss, Collections.emptyMap(), responses); } + private static SocketUtils.ServerCallable buildServerCallableForLog(ServerSocket ss, boolean micro) { + List responses = new ArrayList<>(); + responses.add(micro ? "files/responses/info_response_microseconds.txt" : "files/responses/info_response_no_microseconds2.txt"); + responses.add("files/responses/start_launch_response.txt"); + responses.add("files/responses/simple_response.txt"); // finish item response + return new SocketUtils.ServerCallable(ss, Collections.emptyMap(), responses); + } + /* ---------------------- Launch start ---------------------- */ @Test public void start_launch_useMicroseconds_false_Date_sends_numeric_time() throws Exception { @@ -426,4 +444,56 @@ private void finishLaunchTimeCase(boolean micro, boolean instant) throws Excepti assertTimeNumeric(json, "endTime"); } } + + /* ---------------------- log ---------------------- */ + + @Test + public void log_useMicroseconds_false_Date_sends_numeric_time() throws Exception { + logTimeCase(false, false); + } + + @Test + public void log_launch_useMicroseconds_false_Instant_sends_numeric_time() throws Exception { + logTimeCase(false, true); + } + + @Test + public void log_launch_useMicroseconds_true_Instant_sends_iso_micro_time() throws Exception { + logTimeCase(true, true); + } + + @Test + public void log_launch_useMicroseconds_true_Date_sends_numeric_time() throws Exception { + logTimeCase(true, false); + } + + private void logTimeCase(boolean micro, boolean instant) throws Exception { + ServerSocket ss = SocketUtils.getServerSocketOnFreePort(); + String baseUrl = "http://localhost:" + ss.getLocalPort(); + ListenerParameters parameters = baseParameters(baseUrl); + parameters.setBatchLogsSize(1); + + ReportPortalClient rpClient = Objects.requireNonNull(ReportPortal.builder() + .buildClient(ReportPortalClient.class, parameters, clientExecutor)); + StartLaunchRQ launchRq = buildStartLaunchRq(new Date()); + ReportPortal rp = ReportPortal.create(rpClient, parameters, clientExecutor); + Launch launch = rp.newLaunch(launchRq); + SaveLogRQ rq = buildSaveLogRq(dateOrInstant(instant)); + + SocketUtils.ServerCallable serverCallable = buildServerCallableForLog(ss, micro); + + Pair, ?> result = executeWithClosing( + ss, serverCallable, () -> { + launch.log(rq); + return Boolean.TRUE; + } + ); + + String json = findLastJsonWithKey(result, "time"); + if (expectString(micro, instant)) { + assertTimeIsoMicro(json, "time"); + } else { + assertTimeNumeric(json, "time"); + } + } } diff --git a/src/test/java/com/epam/reportportal/service/OAuth2PasswordGrantAuthInterceptorTest.java b/src/test/java/com/epam/reportportal/service/OAuth2PasswordGrantAuthInterceptorTest.java index 639bfd05..5106f332 100644 --- a/src/test/java/com/epam/reportportal/service/OAuth2PasswordGrantAuthInterceptorTest.java +++ b/src/test/java/com/epam/reportportal/service/OAuth2PasswordGrantAuthInterceptorTest.java @@ -17,7 +17,9 @@ package com.epam.reportportal.service; import com.epam.reportportal.listeners.ListenerParameters; +import jakarta.annotation.Nonnull; import okhttp3.*; +import okhttp3.mockwebserver.MockWebServer; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -547,4 +549,70 @@ public void test401And403ThrottledSimultaneously() throws Exception { assertEquals(3, capturedRequests.size(), "Token refresh should have been attempted after throttling period"); assertEquals(6, apiRequestCount.size(), "API should have been called twice (initial + retry after refresh)"); } + + @Nonnull + private static OAuth2PasswordGrantAuthInterceptor getOAuth2PasswordGrantAuthInterceptor() { + ListenerParameters params = new ListenerParameters(); + params.setOauthTokenUri("https://oauth.example.com/token"); + params.setOauthUsername("test-user"); + params.setOauthPassword("test-password"); + params.setOauthClientId("test-client-id"); + params.setOauthClientSecret("test-client-secret"); + params.setOauthScope("test-scope"); + params.setOauthUseProxy(false); // This is the key setting + + // Create interceptor using constructor that creates its own client + // This will respect the oauthUseProxy setting + return new OAuth2PasswordGrantAuthInterceptor(params); + } + + @Test + public void testOAuthAvoidsProxyWhenOauthUseProxyIsFalse() throws Exception { + // Save original system properties + String originalHttpsProxyHost = System.getProperty("https.proxyHost"); + String originalHttpsProxyPort = System.getProperty("https.proxyPort"); + + try (MockWebServer mockProxyServer = new MockWebServer()) { + // Start mock proxy server on localhost + mockProxyServer.start(); + + // Set system proxy properties to point to our mock proxy + System.setProperty("https.proxyHost", "localhost"); + System.setProperty("https.proxyPort", String.valueOf(mockProxyServer.getPort())); + + // Configure parameters with oauthUseProxy = false and a non-localhost token URL + OAuth2PasswordGrantAuthInterceptor interceptor = getOAuth2PasswordGrantAuthInterceptor(); + + // Create API client with OAuth interceptor + OkHttpClient apiClient = createMockApiClient(interceptor); + + // Execute API request - this will trigger OAuth token request + Request apiRequest = new Request.Builder().url("http://localhost/api/test").get().build(); + + try (Response response = apiClient.newCall(apiRequest).execute()) { + // Verify API response is successful (from our mock API client) + assertTrue(response.isSuccessful()); + assertEquals(200, response.code()); + + // Wait a bit to ensure no request comes to proxy + Thread.sleep(100); + + // Verify that NO requests were made to the proxy server + // because oauthUseProxy=false means OAuth client should bypass proxy + assertEquals(0, mockProxyServer.getRequestCount(), "Proxy server should NOT receive any requests when oauthUseProxy=false"); + } + } finally { + // Restore original system properties + if (originalHttpsProxyHost != null) { + System.setProperty("https.proxyHost", originalHttpsProxyHost); + } else { + System.clearProperty("https.proxyHost"); + } + if (originalHttpsProxyPort != null) { + System.setProperty("https.proxyPort", originalHttpsProxyPort); + } else { + System.clearProperty("https.proxyPort"); + } + } + } } diff --git a/src/test/resources/files/responses/info_response_no_microseconds2.txt b/src/test/resources/files/responses/info_response_no_microseconds2.txt new file mode 100644 index 00000000..ab08c68b --- /dev/null +++ b/src/test/resources/files/responses/info_response_no_microseconds2.txt @@ -0,0 +1,11 @@ +HTTP/1.1 200 OK +Cache-Control: no-cache, no-store, max-age=0, must-revalidate +Content-Type: application/json +Expires: 0 +Pragma: no-cache +X-Content-Type-Options: nosniff +X-Frame-Options: DENY +X-Xss-Protection: 1; mode=block +Content-Length: 30 + +{"build":{"version":"5.12.0"}} \ No newline at end of file