From 7a75720311ddcc3b8fb8e37928cd2e94d8ad7023 Mon Sep 17 00:00:00 2001 From: Kannan J Date: Tue, 1 Sep 2026 06:57:55 +0000 Subject: [PATCH] Revert "Implement gRFC A97: xDS JWT Call Credentials (#12951)" (#13017) This reverts commit 1053f4be9505ae50c9a9678fd23595991cbdd359. We need to address the concerns raised in #13006. --- auth/BUILD.bazel | 1 - auth/build.gradle | 3 +- .../auth/JwtTokenFileCallCredentials.java | 342 --------- .../auth/JwtTokenFileCallCredentialsTest.java | 652 ------------------ .../io/grpc/xds/GrpcXdsTransportFactory.java | 10 +- .../java/io/grpc/xds/client/Bootstrapper.java | 10 +- .../io/grpc/xds/client/BootstrapperImpl.java | 53 +- .../io/grpc/xds/ExtAuthzConfigParserTest.java | 3 +- ...xternalProcessorClientInterceptorTest.java | 2 +- .../grpc/xds/ExternalProcessorFilterTest.java | 2 +- .../java/io/grpc/xds/FaultFilterTest.java | 2 +- .../grpc/xds/GcpAuthenticationFilterTest.java | 2 +- .../io/grpc/xds/GrpcBootstrapperImplTest.java | 256 ------- .../grpc/xds/GrpcServiceConfigParserTest.java | 2 +- .../grpc/xds/GrpcXdsClientImplDataTest.java | 2 +- .../grpc/xds/GrpcXdsClientImplTestBase.java | 22 +- .../grpc/xds/GrpcXdsTransportFactoryTest.java | 63 -- .../test/java/io/grpc/xds/RbacFilterTest.java | 2 +- .../xds/XdsJwtCallCredsIntegrationTest.java | 244 ------- .../java/io/grpc/xds/XdsNameResolverTest.java | 5 +- .../test/java/io/grpc/xds/XdsTestUtils.java | 2 +- .../client/CommonBootstrapperTestUtils.java | 2 +- 22 files changed, 29 insertions(+), 1653 deletions(-) delete mode 100644 auth/src/main/java/io/grpc/auth/JwtTokenFileCallCredentials.java delete mode 100644 auth/src/test/java/io/grpc/auth/JwtTokenFileCallCredentialsTest.java delete mode 100644 xds/src/test/java/io/grpc/xds/XdsJwtCallCredsIntegrationTest.java diff --git a/auth/BUILD.bazel b/auth/BUILD.bazel index 870936ca561..da44243e583 100644 --- a/auth/BUILD.bazel +++ b/auth/BUILD.bazel @@ -11,7 +11,6 @@ java_library( "//api", artifact("com.google.auth:google-auth-library-credentials"), artifact("com.google.code.findbugs:jsr305"), - artifact("com.google.code.gson:gson"), artifact("com.google.guava:guava"), ], ) diff --git a/auth/build.gradle b/auth/build.gradle index 3b80fee476f..d56802c14ca 100644 --- a/auth/build.gradle +++ b/auth/build.gradle @@ -17,8 +17,7 @@ tasks.named("jar").configure { dependencies { api project(':grpc-api'), libraries.google.auth.credentials - implementation libraries.guava, - libraries.gson + implementation libraries.guava testImplementation project(':grpc-testing'), project(':grpc-core'), project(":grpc-context"), // Override google-auth dependency with our newer version diff --git a/auth/src/main/java/io/grpc/auth/JwtTokenFileCallCredentials.java b/auth/src/main/java/io/grpc/auth/JwtTokenFileCallCredentials.java deleted file mode 100644 index eda4a142358..00000000000 --- a/auth/src/main/java/io/grpc/auth/JwtTokenFileCallCredentials.java +++ /dev/null @@ -1,342 +0,0 @@ -/* - * Copyright 2026 The gRPC Authors - * - * 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 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.grpc.auth; - -import static com.google.common.base.Preconditions.checkNotNull; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.io.BaseEncoding; -import com.google.common.io.ByteStreams; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.google.gson.JsonSyntaxException; -import io.grpc.CallCredentials; -import io.grpc.Metadata; -import io.grpc.SecurityLevel; -import io.grpc.Status; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Executor; -import java.util.concurrent.RejectedExecutionException; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * A {@link CallCredentials} implementation that loads a JWT token from a file, - * parses it to extract its expiration time, and caches/refreshes it. - */ -public final class JwtTokenFileCallCredentials extends CallCredentials { - private static final int MAX_FILE_SIZE_BYTES = 1048576; - - private static final Logger log = Logger.getLogger(JwtTokenFileCallCredentials.class.getName()); - - private static final Metadata.Key AUTHORIZATION_HEADER = - Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER); - - private static final long INITIAL_BACKOFF_MILLIS = 1000; - private static final long MAX_BACKOFF_MILLIS = 120000; - private static final double BACKOFF_MULTIPLIER = 1.6; - private static final double JITTER = 0.2; - - private final String filePath; - private final TimeProvider timeProvider; - private final Object lock = new Object(); - - private enum ReadState { - IDLE, - READING, - BACKOFF - } - - private String cachedToken; - private long expirationTimeMillis; - private ReadState readState = ReadState.IDLE; - private Status lastReadFailureStatus; - private long currentBackoffMillis; - private long nextAttemptTimeMillis; - private final List queuedAppliers = new ArrayList<>(); - - interface TimeProvider { - long currentTimeMillis(); - } - - private static final TimeProvider SYSTEM_TIME_PROVIDER = new TimeProvider() { - @Override - public long currentTimeMillis() { - return System.currentTimeMillis(); - } - }; - - public JwtTokenFileCallCredentials(String filePath) { - this(filePath, SYSTEM_TIME_PROVIDER); - } - - @VisibleForTesting - JwtTokenFileCallCredentials(String filePath, TimeProvider timeProvider) { - this.filePath = checkNotNull(filePath, "filePath"); - this.timeProvider = checkNotNull(timeProvider, "timeProvider"); - } - - @Override - public void applyRequestMetadata( - RequestInfo requestInfo, Executor appExecutor, MetadataApplier applier) { - checkNotNull(requestInfo, "requestInfo"); - checkNotNull(appExecutor, "appExecutor"); - checkNotNull(applier, "applier"); - - if (requestInfo.getSecurityLevel() != SecurityLevel.PRIVACY_AND_INTEGRITY) { - applier.fail(Status.UNAUTHENTICATED - .withDescription("Channel security level is not PRIVACY_AND_INTEGRITY")); - return; - } - - long now = timeProvider.currentTimeMillis(); - TokenInfo tokenToApply = null; - boolean triggerRead = false; - Status failStatus = null; - - synchronized (lock) { - if (readState == ReadState.BACKOFF && now >= nextAttemptTimeMillis) { - readState = ReadState.IDLE; - } - - boolean hasValidCache = cachedToken != null && now < expirationTimeMillis; - boolean expiringSoon = hasValidCache && (expirationTimeMillis - now <= 60000); - - if (hasValidCache) { - tokenToApply = new TokenInfo(cachedToken, expirationTimeMillis); - if (expiringSoon && readState == ReadState.IDLE) { - readState = ReadState.READING; - triggerRead = true; - } - } else { - if (readState == ReadState.BACKOFF) { - failStatus = lastReadFailureStatus != null ? lastReadFailureStatus : Status.UNAVAILABLE; - } else { - if (readState == ReadState.IDLE) { - readState = ReadState.READING; - triggerRead = true; - } - queuedAppliers.add(applier); - } - } - } - - if (failStatus != null) { - applier.fail(failStatus); - return; - } - - if (tokenToApply != null) { - Metadata headers = new Metadata(); - headers.put(AUTHORIZATION_HEADER, "Bearer " + tokenToApply.token); - applier.apply(headers); - } - - if (triggerRead) { - try { - appExecutor.execute(new Runnable() { - @Override - public void run() { - loadToken(); - } - }); - } catch (RejectedExecutionException e) { - handleExecutorRejection(e, tokenToApply != null); - } - } - } - - private void handleExecutorRejection( - RejectedExecutionException e, boolean isBackgroundReload) { - log.log(Level.WARNING, "Executor rejected token read task", e); - List appliersToFail = new ArrayList<>(); - synchronized (lock) { - readState = ReadState.IDLE; - if (!isBackgroundReload) { - appliersToFail.addAll(queuedAppliers); - queuedAppliers.clear(); - } - } - for (MetadataApplier applier : appliersToFail) { - try { - applier.fail(Status.UNAVAILABLE - .withDescription("Executor rejected token read task") - .withCause(e)); - } catch (Throwable t) { - log.log(Level.WARNING, "Error calling fail on applier", t); - } - } - } - - private void loadToken() { - TokenInfo tokenInfo = null; - Status status = null; - try { - tokenInfo = readAndParseTokenFile(); - } catch (IOException e) { - status = Status.UNAVAILABLE - .withDescription("Failed to read token file") - .withCause(e); - } catch (IllegalArgumentException e) { - status = Status.UNAUTHENTICATED - .withDescription("Malformed token or invalid claims") - .withCause(e); - } catch (Throwable e) { - status = Status.UNAVAILABLE - .withDescription("Unexpected error loading token") - .withCause(e); - } - - List appliersToApply = new ArrayList<>(); - List appliersToFail = new ArrayList<>(); - - synchronized (lock) { - if (status == null) { - cachedToken = tokenInfo.token; - expirationTimeMillis = tokenInfo.expirationTimeMillis; - readState = ReadState.IDLE; - lastReadFailureStatus = null; - currentBackoffMillis = 0; - nextAttemptTimeMillis = 0; - - appliersToApply.addAll(queuedAppliers); - queuedAppliers.clear(); - } else { - lastReadFailureStatus = status; - readState = ReadState.BACKOFF; - - if (currentBackoffMillis == 0) { - currentBackoffMillis = INITIAL_BACKOFF_MILLIS; - } else { - currentBackoffMillis = Math.min( - (long) (currentBackoffMillis * BACKOFF_MULTIPLIER), MAX_BACKOFF_MILLIS); - } - double uniformRandom = Math.random() * 2 - 1; - long jitteredBackoff = (long) (currentBackoffMillis - + uniformRandom * JITTER * currentBackoffMillis); - nextAttemptTimeMillis = timeProvider.currentTimeMillis() + jitteredBackoff; - - appliersToFail.addAll(queuedAppliers); - queuedAppliers.clear(); - } - } - - if (status == null) { - Metadata headers = new Metadata(); - headers.put(AUTHORIZATION_HEADER, "Bearer " + tokenInfo.token); - for (MetadataApplier applier : appliersToApply) { - try { - applier.apply(headers); - } catch (Throwable t) { - log.log(Level.WARNING, "Error applying credentials", t); - } - } - } else { - log.log(Level.WARNING, "Failed to load token: " + status.getDescription(), status.getCause()); - for (MetadataApplier applier : appliersToFail) { - try { - applier.fail(status); - } catch (Throwable t) { - log.log(Level.WARNING, "Error calling fail on applier", t); - } - } - } - } - - private TokenInfo readAndParseTokenFile() throws IOException { - File file = new File(filePath); - long length = file.length(); - if (length > MAX_FILE_SIZE_BYTES) { - throw new IOException("File size exceeds 1 MB limit: " + length); - } - byte[] bytes; - try (InputStream in = new FileInputStream(file)) { - bytes = ByteStreams.toByteArray( - ByteStreams.limit(in, MAX_FILE_SIZE_BYTES + 1)); - } - if (bytes.length > MAX_FILE_SIZE_BYTES) { - throw new IOException("File size exceeds 1 MB limit: " + bytes.length); - } - String token = new String(bytes, StandardCharsets.UTF_8).trim(); - if (token.isEmpty()) { - throw new IllegalArgumentException("Token file is empty"); - } - String[] segments = token.split("\\.", -1); - if (segments.length != 3) { - throw new IllegalArgumentException("JWT must have 3 segments"); - } - byte[] payloadBytes; - try { - payloadBytes = BaseEncoding.base64Url() - .omitPadding().decode(segments[1]); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException("Invalid Base64URL encoding in payload", e); - } - String payloadJson = new String(payloadBytes, StandardCharsets.UTF_8); - JsonObject jsonObject; - try { - JsonElement jsonElement = JsonParser.parseString(payloadJson); - if (jsonElement == null || !jsonElement.isJsonObject()) { - throw new IllegalArgumentException("Payload is not a JSON object"); - } - jsonObject = jsonElement.getAsJsonObject(); - } catch (JsonSyntaxException e) { - throw new IllegalArgumentException("Invalid JSON payload", e); - } - if (!jsonObject.has("exp")) { - throw new IllegalArgumentException("Payload does not contain 'exp' claim"); - } - JsonElement expElement = jsonObject.get("exp"); - if (!expElement.isJsonPrimitive() || !expElement.getAsJsonPrimitive().isNumber()) { - throw new IllegalArgumentException("'exp' claim is not a number"); - } - long expSeconds = expElement.getAsLong(); - if (expSeconds <= 0) { - throw new IllegalArgumentException("Invalid 'exp' claim value: " + expSeconds); - } - long expirationTimeMillis; - if (expSeconds > Long.MAX_VALUE / 1000) { - expirationTimeMillis = Long.MAX_VALUE; - } else { - expirationTimeMillis = (expSeconds - 30) * 1000; - } - return new TokenInfo(token, expirationTimeMillis); - } - - @Override - @SuppressWarnings("deprecation") - public void thisUsesUnstableApi() { - // Yes - } - - private static class TokenInfo { - final String token; - final long expirationTimeMillis; - - TokenInfo(String token, long expirationTimeMillis) { - this.token = token; - this.expirationTimeMillis = expirationTimeMillis; - } - } -} diff --git a/auth/src/test/java/io/grpc/auth/JwtTokenFileCallCredentialsTest.java b/auth/src/test/java/io/grpc/auth/JwtTokenFileCallCredentialsTest.java deleted file mode 100644 index a5aa70e32cb..00000000000 --- a/auth/src/test/java/io/grpc/auth/JwtTokenFileCallCredentialsTest.java +++ /dev/null @@ -1,652 +0,0 @@ -/* - * Copyright 2026 The gRPC Authors - * - * 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 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.grpc.auth; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; - -import io.grpc.Attributes; -import io.grpc.CallCredentials; -import io.grpc.CallCredentials.MetadataApplier; -import io.grpc.Metadata; -import io.grpc.MethodDescriptor; -import io.grpc.SecurityLevel; -import io.grpc.Status; -import io.grpc.testing.TestMethodDescriptors; -import java.io.File; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Executor; -import java.util.concurrent.RejectedExecutionException; -import org.junit.After; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; - -@RunWith(JUnit4.class) -public class JwtTokenFileCallCredentialsTest { - - @Rule public final MockitoRule mocks = MockitoJUnit.rule(); - @Rule public final TemporaryFolder tempFolder = new TemporaryFolder(); - - @Mock private MetadataApplier applier1; - @Mock private MetadataApplier applier2; - - @Captor private ArgumentCaptor headersCaptor; - @Captor private ArgumentCaptor statusCaptor; - - private static final Metadata.Key AUTHORIZATION_HEADER = - Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER); - - private FakeTimeProvider timeProvider; - private FakeExecutor executor; - - @Before - public void setUp() { - timeProvider = new FakeTimeProvider(); - executor = new FakeExecutor(); - } - - @After - public void tearDown() { - assertEquals(0, executor.runnables.size()); - } - - private String createJwtToken(long expSeconds) { - String header = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9"; // {"alg":"RS256","typ":"JWT"} - String payload = com.google.common.io.BaseEncoding.base64Url().omitPadding().encode( - ("{\"exp\":" + expSeconds + "}").getBytes(StandardCharsets.UTF_8)); - String signature = "signature"; - return header + "." + payload + "." + signature; - } - - private File writeTokenToFile(String content) throws IOException { - File file = tempFolder.newFile(); - java.io.FileOutputStream fos = new java.io.FileOutputStream(file); - try { - fos.write(content.getBytes(StandardCharsets.UTF_8)); - } finally { - fos.close(); - } - return file; - } - - private void updateTokenFile(File file, String content) throws IOException { - java.io.FileOutputStream fos = new java.io.FileOutputStream(file); - try { - fos.write(content.getBytes(StandardCharsets.UTF_8)); - } finally { - fos.close(); - } - } - - @Test - public void applyMetadata_insecureChannel_fails() throws Exception { - long nowSecs = timeProvider.currentTimeMillis() / 1000; - File tokenFile = writeTokenToFile(createJwtToken(nowSecs + 1000)); - JwtTokenFileCallCredentials credentials = - new JwtTokenFileCallCredentials(tokenFile.getAbsolutePath(), timeProvider); - - RequestInfoImpl requestInfo = new RequestInfoImpl(SecurityLevel.NONE); - credentials.applyRequestMetadata(requestInfo, executor, applier1); - - verify(applier1).fail(statusCaptor.capture()); - Status status = statusCaptor.getValue(); - assertEquals(Status.Code.UNAUTHENTICATED, status.getCode()); - assertTrue(status.getDescription() - .contains("Channel security level is not PRIVACY_AND_INTEGRITY")); - } - - @Test - public void applyMetadata_validCachedToken_cacheHit() throws Exception { - String token = createJwtToken(timeProvider.currentTimeMillis() / 1000 + 1000); - File tokenFile = writeTokenToFile(token); - JwtTokenFileCallCredentials credentials = - new JwtTokenFileCallCredentials(tokenFile.getAbsolutePath(), timeProvider); - - RequestInfoImpl requestInfo = new RequestInfoImpl(SecurityLevel.PRIVACY_AND_INTEGRITY); - - // First load to populate the cache - credentials.applyRequestMetadata(requestInfo, executor, applier1); - assertEquals(1, executor.runnables.size()); - executor.runNext(); - - verify(applier1).apply(headersCaptor.capture()); - assertEquals("Bearer " + token, headersCaptor.getValue().get(AUTHORIZATION_HEADER)); - - // Second load - should be synchronous cache hit - credentials.applyRequestMetadata(requestInfo, executor, applier2); - // Executor should NOT have any new runnables - assertEquals(0, executor.runnables.size()); - - verify(applier2).apply(headersCaptor.capture()); - assertEquals("Bearer " + token, headersCaptor.getValue().get(AUTHORIZATION_HEADER)); - } - - @Test - public void applyMetadata_tokenExpiringSoon_triggersBackgroundRefresh() throws Exception { - timeProvider.set(100_000); - // exp = 180 seconds, meaning expirationTimeMillis = (180-30)*1000 = 150_000. - String firstToken = createJwtToken(180); - File tokenFile = writeTokenToFile(firstToken); - - JwtTokenFileCallCredentials credentials = - new JwtTokenFileCallCredentials(tokenFile.getAbsolutePath(), timeProvider); - - RequestInfoImpl requestInfo = new RequestInfoImpl(SecurityLevel.PRIVACY_AND_INTEGRITY); - - // First load to populate the cache - credentials.applyRequestMetadata(requestInfo, executor, applier1); - assertEquals(1, executor.runnables.size()); - executor.runNext(); - - verify(applier1).apply(headersCaptor.capture()); - assertEquals("Bearer " + firstToken, headersCaptor.getValue().get(AUTHORIZATION_HEADER)); - - // Update the token file with a new token - // exp = 1000 seconds, meaning expirationTimeMillis = 970_000. - String secondToken = createJwtToken(1000); - updateTokenFile(tokenFile, secondToken); - - // Call apply again. Time hasn't changed (still 100_000), so firstToken is valid - // (expires at 150_000) but expiring soon (150_000 - 100_000 = 50_000 <= 60_000). - // It should synchronously apply firstToken and queue a background read. - credentials.applyRequestMetadata(requestInfo, executor, applier2); - - // Check that it synchronously applied the first token - verify(applier2).apply(headersCaptor.capture()); - assertEquals("Bearer " + firstToken, headersCaptor.getValue().get(AUTHORIZATION_HEADER)); - - // And triggered a background read - assertEquals(1, executor.runnables.size()); - executor.runNext(); - - // Now, a subsequent call should return the second (newly cached) token synchronously! - MetadataApplier applier3 = Mockito.mock(MetadataApplier.class); - credentials.applyRequestMetadata(requestInfo, executor, applier3); - assertEquals(0, executor.runnables.size()); - - ArgumentCaptor headersCaptor3 = ArgumentCaptor.forClass(Metadata.class); - verify(applier3).apply(headersCaptor3.capture()); - assertEquals("Bearer " + secondToken, headersCaptor3.getValue().get(AUTHORIZATION_HEADER)); - } - - @Test - public void applyMetadata_concurrentCalls_queued() throws Exception { - String token = createJwtToken(timeProvider.currentTimeMillis() / 1000 + 1000); - File tokenFile = writeTokenToFile(token); - JwtTokenFileCallCredentials credentials = - new JwtTokenFileCallCredentials(tokenFile.getAbsolutePath(), timeProvider); - - RequestInfoImpl requestInfo = new RequestInfoImpl(SecurityLevel.PRIVACY_AND_INTEGRITY); - - // First call starts loading - credentials.applyRequestMetadata(requestInfo, executor, applier1); - assertEquals(1, executor.runnables.size()); - - // Second call while loading is in progress - credentials.applyRequestMetadata(requestInfo, executor, applier2); - // Executor should still have only 1 runnable - assertEquals(1, executor.runnables.size()); - - // Neither applier should have received a token or failed yet - verify(applier1, never()).apply(any()); - verify(applier1, never()).fail(any()); - verify(applier2, never()).apply(any()); - verify(applier2, never()).fail(any()); - - // Run the loader runnable - executor.runNext(); - - // Both appliers should now be successfully invoked - verify(applier1).apply(headersCaptor.capture()); - assertEquals("Bearer " + token, headersCaptor.getValue().get(AUTHORIZATION_HEADER)); - - ArgumentCaptor headersCaptor2 = ArgumentCaptor.forClass(Metadata.class); - verify(applier2).apply(headersCaptor2.capture()); - assertEquals("Bearer " + token, headersCaptor2.getValue().get(AUTHORIZATION_HEADER)); - } - - @Test - public void applyMetadata_fileNotFound_failsUnavailable() throws Exception { - JwtTokenFileCallCredentials credentials = - new JwtTokenFileCallCredentials( - tempFolder.getRoot().getAbsolutePath() + "/non-existent.txt", timeProvider); - - RequestInfoImpl requestInfo = new RequestInfoImpl(SecurityLevel.PRIVACY_AND_INTEGRITY); - - credentials.applyRequestMetadata(requestInfo, executor, applier1); - assertEquals(1, executor.runnables.size()); - executor.runNext(); - - verify(applier1).fail(statusCaptor.capture()); - Status status = statusCaptor.getValue(); - assertEquals(Status.Code.UNAVAILABLE, status.getCode()); - assertTrue(status.getCause() instanceof IOException); - } - - @Test - public void applyMetadata_fileReadError_backoff() throws Exception { - JwtTokenFileCallCredentials credentials = - new JwtTokenFileCallCredentials( - tempFolder.getRoot().getAbsolutePath() + "/non-existent.txt", timeProvider); - - RequestInfoImpl requestInfo = new RequestInfoImpl(SecurityLevel.PRIVACY_AND_INTEGRITY); - - // 1. First attempt fails - timeProvider.set(10000); - credentials.applyRequestMetadata(requestInfo, executor, applier1); - assertEquals(1, executor.runnables.size()); - executor.runNext(); - - verify(applier1).fail(statusCaptor.capture()); - Status firstStatus = statusCaptor.getValue(); - assertEquals(Status.Code.UNAVAILABLE, firstStatus.getCode()); - - // 2. Call again before backoff expires (at t=10500) - timeProvider.set(10500); - credentials.applyRequestMetadata(requestInfo, executor, applier2); - // Should fail synchronously (fail-fast) - verify(applier2).fail(statusCaptor.capture()); - Status secondStatus = statusCaptor.getValue(); - assertEquals(Status.Code.UNAVAILABLE, secondStatus.getCode()); - assertEquals(firstStatus.getDescription(), secondStatus.getDescription()); - assertEquals(firstStatus.getCause(), secondStatus.getCause()); - // Executor should NOT have been invoked - assertEquals(0, executor.runnables.size()); - - // 3. Move time past backoff limit (t=11001) - timeProvider.set(15000); - MetadataApplier applier3 = Mockito.mock(MetadataApplier.class); - credentials.applyRequestMetadata(requestInfo, executor, applier3); - // Should NOT fail fast. Instead, it should trigger a new attempt. - assertEquals(1, executor.runnables.size()); - // Clean up the task from executor list - executor.runNext(); - } - - @Test - public void applyMetadata_malformedJwt_failsUnauthenticated() throws Exception { - RequestInfoImpl requestInfo = new RequestInfoImpl(SecurityLevel.PRIVACY_AND_INTEGRITY); - - // Case 1: Invalid segment count - File tokenFile1 = writeTokenToFile("header.payload"); // Only 2 segments - JwtTokenFileCallCredentials credentials1 = - new JwtTokenFileCallCredentials(tokenFile1.getAbsolutePath(), timeProvider); - credentials1.applyRequestMetadata(requestInfo, executor, applier1); - assertEquals(1, executor.runnables.size()); - executor.runNext(); - verify(applier1).fail(statusCaptor.capture()); - assertEquals(Status.Code.UNAUTHENTICATED, statusCaptor.getValue().getCode()); - assertTrue(statusCaptor.getValue().getDescription().contains("Malformed token")); - - // Case 2: Invalid base64url payload segment - File tokenFile2 = writeTokenToFile("header.invalid_base64_symbols#$%.signature"); - JwtTokenFileCallCredentials credentials2 = - new JwtTokenFileCallCredentials(tokenFile2.getAbsolutePath(), timeProvider); - MetadataApplier applier2 = Mockito.mock(MetadataApplier.class); - ArgumentCaptor statusCaptor2 = ArgumentCaptor.forClass(Status.class); - credentials2.applyRequestMetadata(requestInfo, executor, applier2); - assertEquals(1, executor.runnables.size()); - executor.runNext(); - verify(applier2).fail(statusCaptor2.capture()); - assertEquals(Status.Code.UNAUTHENTICATED, statusCaptor2.getValue().getCode()); - assertTrue(statusCaptor2.getValue().getDescription().contains("Malformed token")); - } - - @Test - public void applyMetadata_missingExpClaim_failsUnauthenticated() throws Exception { - RequestInfoImpl requestInfo = new RequestInfoImpl(SecurityLevel.PRIVACY_AND_INTEGRITY); - - // Case 1: Missing exp claim - String header = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9"; - String payloadNoExp = com.google.common.io.BaseEncoding.base64Url().omitPadding().encode( - "{\"foo\":\"bar\"}".getBytes(StandardCharsets.UTF_8)); - File tokenFile1 = writeTokenToFile(header + "." + payloadNoExp + ".signature"); - JwtTokenFileCallCredentials credentials1 = - new JwtTokenFileCallCredentials(tokenFile1.getAbsolutePath(), timeProvider); - credentials1.applyRequestMetadata(requestInfo, executor, applier1); - assertEquals(1, executor.runnables.size()); - executor.runNext(); - verify(applier1).fail(statusCaptor.capture()); - assertEquals(Status.Code.UNAUTHENTICATED, statusCaptor.getValue().getCode()); - assertTrue(statusCaptor.getValue().getDescription() - .contains("Malformed token or invalid claims")); - - // Case 2: exp is not a number - String payloadExpString = com.google.common.io.BaseEncoding.base64Url().omitPadding().encode( - "{\"exp\":\"not-a-number\"}".getBytes(StandardCharsets.UTF_8)); - File tokenFile2 = writeTokenToFile(header + "." + payloadExpString + ".signature"); - JwtTokenFileCallCredentials credentials2 = - new JwtTokenFileCallCredentials(tokenFile2.getAbsolutePath(), timeProvider); - MetadataApplier applier2 = Mockito.mock(MetadataApplier.class); - ArgumentCaptor statusCaptor2 = ArgumentCaptor.forClass(Status.class); - credentials2.applyRequestMetadata(requestInfo, executor, applier2); - assertEquals(1, executor.runnables.size()); - executor.runNext(); - verify(applier2).fail(statusCaptor2.capture()); - assertEquals(Status.Code.UNAUTHENTICATED, statusCaptor2.getValue().getCode()); - assertTrue(statusCaptor2.getValue().getDescription() - .contains("Malformed token or invalid claims")); - - // Case 3: exp is <= 0 - String payloadExpNegative = com.google.common.io.BaseEncoding.base64Url().omitPadding().encode( - "{\"exp\":-10}".getBytes(StandardCharsets.UTF_8)); - File tokenFile3 = writeTokenToFile(header + "." + payloadExpNegative + ".signature"); - JwtTokenFileCallCredentials credentials3 = - new JwtTokenFileCallCredentials(tokenFile3.getAbsolutePath(), timeProvider); - MetadataApplier applier3 = Mockito.mock(MetadataApplier.class); - ArgumentCaptor statusCaptor3 = ArgumentCaptor.forClass(Status.class); - credentials3.applyRequestMetadata(requestInfo, executor, applier3); - assertEquals(1, executor.runnables.size()); - executor.runNext(); - verify(applier3).fail(statusCaptor3.capture()); - assertEquals(Status.Code.UNAUTHENTICATED, statusCaptor3.getValue().getCode()); - assertTrue(statusCaptor3.getValue().getDescription() - .contains("Malformed token or invalid claims")); - } - - @Test - public void applyMetadata_fileTooLarge_failsUnavailable() throws Exception { - byte[] largeContent = new byte[1048577]; - java.util.Arrays.fill(largeContent, (byte) 'a'); - File tokenFile = tempFolder.newFile("large-token.txt"); - java.io.FileOutputStream fos = new java.io.FileOutputStream(tokenFile); - try { - fos.write(largeContent); - } finally { - fos.close(); - } - - JwtTokenFileCallCredentials credentials = - new JwtTokenFileCallCredentials(tokenFile.getAbsolutePath(), timeProvider); - - RequestInfoImpl requestInfo = new RequestInfoImpl(SecurityLevel.PRIVACY_AND_INTEGRITY); - credentials.applyRequestMetadata(requestInfo, executor, applier1); - assertEquals(1, executor.runnables.size()); - executor.runNext(); - - verify(applier1).fail(statusCaptor.capture()); - Status status = statusCaptor.getValue(); - assertEquals(Status.Code.UNAVAILABLE, status.getCode()); - assertTrue(status.getDescription().contains("Failed to read token file")); - } - - @Test - public void applyMetadata_executorRejection_failsUnavailable() throws Exception { - long nowSecs = timeProvider.currentTimeMillis() / 1000; - File tokenFile = writeTokenToFile(createJwtToken(nowSecs + 1000)); - JwtTokenFileCallCredentials credentials = - new JwtTokenFileCallCredentials(tokenFile.getAbsolutePath(), timeProvider); - - RequestInfoImpl requestInfo = new RequestInfoImpl(SecurityLevel.PRIVACY_AND_INTEGRITY); - - Executor rejectingExecutor = new Executor() { - @Override - public void execute(Runnable command) { - throw new RejectedExecutionException("Rejected!"); - } - }; - - credentials.applyRequestMetadata(requestInfo, rejectingExecutor, applier1); - - assertEquals(0, executor.runnables.size()); - - verify(applier1).fail(statusCaptor.capture()); - Status status = statusCaptor.getValue(); - assertEquals(Status.Code.UNAVAILABLE, status.getCode()); - assertTrue(status.getDescription().contains("Executor rejected token read task")); - assertTrue(status.getCause() instanceof RejectedExecutionException); - } - - @Test - public void applyMetadata_executorRejection_duringBackgroundRefresh_doesNotFail() - throws Exception { - timeProvider.set(100_000); - String firstToken = createJwtToken(180); - File tokenFile = writeTokenToFile(firstToken); - - JwtTokenFileCallCredentials credentials = - new JwtTokenFileCallCredentials(tokenFile.getAbsolutePath(), timeProvider); - - RequestInfoImpl requestInfo = new RequestInfoImpl(SecurityLevel.PRIVACY_AND_INTEGRITY); - - // First load to populate the cache - credentials.applyRequestMetadata(requestInfo, executor, applier1); - assertEquals(1, executor.runnables.size()); - executor.runNext(); - verify(applier1).apply(any()); - - Executor rejectingExecutor = new Executor() { - @Override - public void execute(Runnable command) { - throw new RejectedExecutionException("Rejected!"); - } - }; - - // Second call triggers background refresh. - // It should synchronously apply the cached token, and attempt background refresh. - // The background refresh fails to execute, but the applier should succeed! - credentials.applyRequestMetadata(requestInfo, rejectingExecutor, applier2); - - verify(applier2).apply(headersCaptor.capture()); - assertEquals("Bearer " + firstToken, headersCaptor.getValue().get(AUTHORIZATION_HEADER)); - verify(applier2, never()).fail(any()); - } - - @Test - public void applyMetadata_backgroundRefreshFails_servesCachedTokenDuringBackoff() - throws Exception { - timeProvider.set(100_000); - String firstToken = createJwtToken(180); // expires at 150_000 - File tokenFile = writeTokenToFile(firstToken); - - JwtTokenFileCallCredentials credentials = - new JwtTokenFileCallCredentials(tokenFile.getAbsolutePath(), timeProvider); - - RequestInfoImpl requestInfo = new RequestInfoImpl(SecurityLevel.PRIVACY_AND_INTEGRITY); - - // 1. Populate the cache - credentials.applyRequestMetadata(requestInfo, executor, applier1); - assertEquals(1, executor.runnables.size()); - executor.runNext(); - verify(applier1).apply(any()); - - // Update the token file with an invalid one - updateTokenFile(tokenFile, "invalid-token"); - - // 2. Call again - it's expiring soon since 150_000 - 100_000 <= 60_000 - credentials.applyRequestMetadata(requestInfo, executor, applier2); - // Applies synchronously - verify(applier2).apply(headersCaptor.capture()); - assertEquals("Bearer " + firstToken, headersCaptor.getValue().get(AUTHORIZATION_HEADER)); - - // Background task queued - assertEquals(1, executor.runnables.size()); - executor.runNext(); // Task runs and fails, putting it in BACKOFF state - - // 3. Call again while in BACKOFF state. Time hasn't changed, still 100_000. - // Token is still valid. - MetadataApplier applier3 = Mockito.mock(MetadataApplier.class); - credentials.applyRequestMetadata(requestInfo, executor, applier3); - - // No new background tasks should be scheduled because it's in BACKOFF - assertEquals(0, executor.runnables.size()); - - // It should STILL serve the cached token because it is valid - ArgumentCaptor headersCaptor3 = ArgumentCaptor.forClass(Metadata.class); - verify(applier3).apply(headersCaptor3.capture()); - assertEquals("Bearer " + firstToken, headersCaptor3.getValue().get(AUTHORIZATION_HEADER)); - verify(applier3, never()).fail(any()); - - // Move time past backoff limit - timeProvider.set(105_000); // 105_000 > 100_000 + 1000 - - // 4. Call again. Time is past backoff and expiring soon. - // It should serve cache AND schedule refresh. - MetadataApplier applier4 = Mockito.mock(MetadataApplier.class); - credentials.applyRequestMetadata(requestInfo, executor, applier4); - - assertEquals(1, executor.runnables.size()); - executor.runNext(); // Consume the failing background task again - - ArgumentCaptor headersCaptor4 = ArgumentCaptor.forClass(Metadata.class); - verify(applier4).apply(headersCaptor4.capture()); - assertEquals("Bearer " + firstToken, headersCaptor4.getValue().get(AUTHORIZATION_HEADER)); - } - - private static class FakeTimeProvider implements JwtTokenFileCallCredentials.TimeProvider { - private long currentTimeMillis = 0; - - @Override - public long currentTimeMillis() { - return currentTimeMillis; - } - - void set(long timeMillis) { - currentTimeMillis = timeMillis; - } - } - - private static class FakeExecutor implements Executor { - private final List runnables = new ArrayList<>(); - - @Override - public void execute(Runnable command) { - runnables.add(command); - } - - void runNext() { - if (runnables.isEmpty()) { - throw new IllegalStateException("No runnables queued"); - } - runnables.remove(0).run(); - } - } - - private static final class RequestInfoImpl extends CallCredentials.RequestInfo { - private final SecurityLevel securityLevel; - - RequestInfoImpl(SecurityLevel securityLevel) { - this.securityLevel = securityLevel; - } - - @Override - public MethodDescriptor getMethodDescriptor() { - return MethodDescriptor.newBuilder() - .setType(MethodDescriptor.MethodType.UNKNOWN) - .setFullMethodName("a.service/method") - .setRequestMarshaller(TestMethodDescriptors.voidMarshaller()) - .setResponseMarshaller(TestMethodDescriptors.voidMarshaller()) - .build(); - } - - @Override - public SecurityLevel getSecurityLevel() { - return securityLevel; - } - - @Override - public String getAuthority() { - return "testauthority"; - } - - @Override - public Attributes getTransportAttrs() { - return Attributes.EMPTY; - } - } - - @Test - public void applyMetadata_fileContentTooLarge_failsUnavailable() throws Exception { - File devZero = new File("/dev/zero"); - org.junit.Assume.assumeTrue(devZero.exists()); - - JwtTokenFileCallCredentials credentials = - new JwtTokenFileCallCredentials(devZero.getAbsolutePath(), timeProvider); - - RequestInfoImpl requestInfo = new RequestInfoImpl(SecurityLevel.PRIVACY_AND_INTEGRITY); - credentials.applyRequestMetadata(requestInfo, executor, applier1); - assertEquals(1, executor.runnables.size()); - executor.runNext(); - - verify(applier1).fail(statusCaptor.capture()); - Status status = statusCaptor.getValue(); - assertEquals(Status.Code.UNAVAILABLE, status.getCode()); - assertTrue(status.getDescription().contains("Failed to read token file")); - assertTrue(status.getCause().getMessage().contains("File size exceeds 1 MB limit")); - } - - @Test - public void emptyTokenFileThrowsIllegalArgumentException() throws Exception { - File emptyFile = File.createTempFile("emptyToken", ".jwt", tempFolder.getRoot()); - emptyFile.deleteOnExit(); - - JwtTokenFileCallCredentials credentials = - new JwtTokenFileCallCredentials(emptyFile.getAbsolutePath(), timeProvider); - - RequestInfoImpl requestInfo = new RequestInfoImpl(SecurityLevel.PRIVACY_AND_INTEGRITY); - credentials.applyRequestMetadata(requestInfo, executor, applier1); - assertEquals(1, executor.runnables.size()); - executor.runNext(); - - verify(applier1).fail(statusCaptor.capture()); - Status status = statusCaptor.getValue(); - assertEquals(Status.Code.UNAUTHENTICATED, status.getCode()); - assertTrue(status.getCause() instanceof IllegalArgumentException); - } - - @Test - public void largeExpClaimClampsToLongMaxValue() throws Exception { - String header = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0"; // {"alg":"none","typ":"JWT"} - String payload = "eyJleHAiOjk5OTk5OTk5OTk5OTk5OTl9"; // {"exp":9999999999999999} - String token = header + "." + payload + "."; - - File tokenFile = File.createTempFile("largeExpToken", ".jwt", tempFolder.getRoot()); - tokenFile.deleteOnExit(); - java.nio.file.Files.write(tokenFile.toPath(), - token.getBytes(java.nio.charset.StandardCharsets.UTF_8)); - - timeProvider.currentTimeMillis = 0; - JwtTokenFileCallCredentials credentials = - new JwtTokenFileCallCredentials(tokenFile.getAbsolutePath(), timeProvider); - - RequestInfoImpl requestInfo = new RequestInfoImpl(SecurityLevel.PRIVACY_AND_INTEGRITY); - credentials.applyRequestMetadata(requestInfo, executor, applier1); - assertEquals(1, executor.runnables.size()); - executor.runNext(); - - verify(applier1).apply(any(io.grpc.Metadata.class)); - // The credentials logic doesn't easily expose the clamped value directly to this test without - // waiting or mocking, but it prevents overflow crash - // and parses successfully instead of returning an error. - } -} diff --git a/xds/src/main/java/io/grpc/xds/GrpcXdsTransportFactory.java b/xds/src/main/java/io/grpc/xds/GrpcXdsTransportFactory.java index 13ff70432ed..3db088435f9 100644 --- a/xds/src/main/java/io/grpc/xds/GrpcXdsTransportFactory.java +++ b/xds/src/main/java/io/grpc/xds/GrpcXdsTransportFactory.java @@ -24,7 +24,6 @@ import io.grpc.ChannelConfigurator; import io.grpc.ChannelCredentials; import io.grpc.ClientCall; -import io.grpc.CompositeCallCredentials; import io.grpc.Context; import io.grpc.Grpc; import io.grpc.ManagedChannel; @@ -88,14 +87,7 @@ public GrpcXdsTransport(Bootstrapper.ServerInfo serverInfo, channelBuilder.childChannelConfigurator(channelConfigurator); } this.channel = channelBuilder.build(); - if (callCredentials != null && serverInfo.callCredentials() != null) { - this.callCredentials = new CompositeCallCredentials( - callCredentials, serverInfo.callCredentials()); - } else if (serverInfo.callCredentials() != null) { - this.callCredentials = serverInfo.callCredentials(); - } else { - this.callCredentials = callCredentials; - } + this.callCredentials = callCredentials; } @VisibleForTesting diff --git a/xds/src/main/java/io/grpc/xds/client/Bootstrapper.java b/xds/src/main/java/io/grpc/xds/client/Bootstrapper.java index 843c6f3f3c5..b8d6444e3b3 100644 --- a/xds/src/main/java/io/grpc/xds/client/Bootstrapper.java +++ b/xds/src/main/java/io/grpc/xds/client/Bootstrapper.java @@ -22,7 +22,6 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import io.grpc.CallCredentials; import io.grpc.Internal; import io.grpc.xds.client.EnvoyProtoData.Node; import java.util.List; @@ -69,23 +68,20 @@ public abstract static class ServerInfo { public abstract boolean failOnDataErrors(); - @Nullable public abstract CallCredentials callCredentials(); - @VisibleForTesting public static ServerInfo create(String target, @Nullable Object implSpecificConfig) { return new AutoValue_Bootstrapper_ServerInfo(target, implSpecificConfig, - false, false, false, false, null); + false, false, false, false); } @VisibleForTesting public static ServerInfo create( String target, Object implSpecificConfig, boolean ignoreResourceDeletion, boolean isTrustedXdsServer, - boolean resourceTimerIsTransientError, boolean failOnDataErrors, - @Nullable CallCredentials callCredentials) { + boolean resourceTimerIsTransientError, boolean failOnDataErrors) { return new AutoValue_Bootstrapper_ServerInfo(target, implSpecificConfig, ignoreResourceDeletion, isTrustedXdsServer, - resourceTimerIsTransientError, failOnDataErrors, callCredentials); + resourceTimerIsTransientError, failOnDataErrors); } } diff --git a/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java b/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java index 8d5d3471b9d..3f4ea8eb5c6 100644 --- a/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java +++ b/xds/src/main/java/io/grpc/xds/client/BootstrapperImpl.java @@ -19,11 +19,8 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import io.grpc.CallCredentials; -import io.grpc.CompositeCallCredentials; import io.grpc.Internal; import io.grpc.InternalLogId; -import io.grpc.auth.JwtTokenFileCallCredentials; import io.grpc.internal.GrpcUtil; import io.grpc.internal.GrpcUtil.GrpcBuildVersion; import io.grpc.internal.JsonParser; @@ -34,7 +31,6 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -69,10 +65,6 @@ public abstract class BootstrapperImpl extends Bootstrapper { @VisibleForTesting static boolean enableXdsFallback = GrpcUtil.getFlag(GRPC_EXPERIMENTAL_XDS_FALLBACK, true); - @VisibleForTesting - public static boolean enableXdsBootstrapCallCreds = GrpcUtil.getFlag( - "GRPC_EXPERIMENTAL_XDS_BOOTSTRAP_CALL_CREDS", false); - @VisibleForTesting public static boolean xdsDataErrorHandlingEnabled = GrpcUtil.getFlag(GRPC_EXPERIMENTAL_XDS_DATA_ERROR_HANDLING, false); @@ -291,58 +283,15 @@ private List parseServerInfos(List rawServerConfigs, XdsLogger lo failOnDataErrors = xdsDataErrorHandlingEnabled && serverFeatures.contains(SERVER_FEATURE_FAIL_ON_DATA_ERRORS); } - CallCredentials callCredentials = null; - List rawCallCreds = JsonUtil.getList(serverConfig, "call_creds"); - if (enableXdsBootstrapCallCreds && rawCallCreds != null) { - List> callCredsList = JsonUtil.checkObjectList(rawCallCreds); - callCredentials = parseCallCredentials(callCredsList, serverUri); - } servers.add( ServerInfo.create(serverUri, implSpecificConfig, ignoreResourceDeletion, serverFeatures != null && serverFeatures.contains(SERVER_FEATURE_TRUSTED_XDS_SERVER), - resourceTimerIsTransientError, failOnDataErrors, callCredentials)); + resourceTimerIsTransientError, failOnDataErrors)); } return servers.build(); } - @Nullable - private CallCredentials parseCallCredentials(List> jsonList, String serverUri) - throws XdsInitializationException { - List parsedCreds = new ArrayList<>(); - for (Map credJson : jsonList) { - String type = JsonUtil.getString(credJson, "type"); - if (type == null) { - throw new XdsInitializationException( - "Invalid bootstrap: server " + serverUri + " with 'call_creds' type unspecified"); - } - if ("jwt_token_file".equals(type)) { - Map config = JsonUtil.getObject(credJson, "config"); - if (config == null) { - throw new XdsInitializationException( - "Invalid bootstrap: server " + serverUri + " with 'jwt_token_file' config missing"); - } - String jwtTokenFile = JsonUtil.getString(config, "jwt_token_file"); - if (jwtTokenFile == null || jwtTokenFile.isEmpty()) { - throw new XdsInitializationException( - "Invalid bootstrap: server " + serverUri - + " with 'jwt_token_file' jwt_token_file missing or empty"); - } - parsedCreds.add(new JwtTokenFileCallCredentials(jwtTokenFile)); - } else { - logger.log(XdsLogLevel.INFO, "Skipping unsupported call credential type: {0}", type); - } - } - if (parsedCreds.isEmpty()) { - return null; - } - CallCredentials combined = parsedCreds.get(0); - for (int i = 1; i < parsedCreds.size(); i++) { - combined = new CompositeCallCredentials(combined, parsedCreds.get(i)); - } - return combined; - } - @VisibleForTesting public void setFileReader(FileReader reader) { this.reader = reader; diff --git a/xds/src/test/java/io/grpc/xds/ExtAuthzConfigParserTest.java b/xds/src/test/java/io/grpc/xds/ExtAuthzConfigParserTest.java index 3340586e3ea..fa2718cbe63 100644 --- a/xds/src/test/java/io/grpc/xds/ExtAuthzConfigParserTest.java +++ b/xds/src/test/java/io/grpc/xds/ExtAuthzConfigParserTest.java @@ -64,8 +64,7 @@ private static BootstrapInfo dummyBootstrapInfo() { } private static ServerInfo dummyServerInfo() { - return ServerInfo.create( - "test_target", Collections.emptyMap(), false, true, false, false, null); + return ServerInfo.create("test_target", Collections.emptyMap(), false, true, false, false); } private ExtAuthz.Builder extAuthzBuilder; diff --git a/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java b/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java index cd6de138a48..2e8761214a3 100644 --- a/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java +++ b/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java @@ -246,7 +246,7 @@ public void setUp() throws Exception { serverInfo = Bootstrapper.ServerInfo.create( - "test_target", Collections.emptyMap(), false, true, false, false, null); + "test_target", Collections.emptyMap(), false, true, false, false); filterContext = Filter.FilterConfigParseContext.builder() .bootstrapInfo(bootstrapInfo) diff --git a/xds/src/test/java/io/grpc/xds/ExternalProcessorFilterTest.java b/xds/src/test/java/io/grpc/xds/ExternalProcessorFilterTest.java index db42217d08c..74853f1edd0 100644 --- a/xds/src/test/java/io/grpc/xds/ExternalProcessorFilterTest.java +++ b/xds/src/test/java/io/grpc/xds/ExternalProcessorFilterTest.java @@ -77,7 +77,7 @@ public void setUp() throws Exception { serverInfo = Bootstrapper.ServerInfo.create( - "test_target", Collections.emptyMap(), false, true, false, false, null); + "test_target", Collections.emptyMap(), false, true, false, false); filterContext = Filter.FilterConfigParseContext.builder() .bootstrapInfo(bootstrapInfo) diff --git a/xds/src/test/java/io/grpc/xds/FaultFilterTest.java b/xds/src/test/java/io/grpc/xds/FaultFilterTest.java index 630a8f8bd2b..9033d1e636e 100644 --- a/xds/src/test/java/io/grpc/xds/FaultFilterTest.java +++ b/xds/src/test/java/io/grpc/xds/FaultFilterTest.java @@ -114,7 +114,7 @@ private static Filter.FilterConfigParseContext getFilterContext() { .node(Node.newBuilder().build()) .build()) .serverInfo(ServerInfo.create( - "test_target", Collections.emptyMap(), false, true, false, false, null)) + "test_target", Collections.emptyMap(), false, true, false, false)) .build(); } } diff --git a/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java b/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java index a53335a7c9b..11745c01fc2 100644 --- a/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java +++ b/xds/src/test/java/io/grpc/xds/GcpAuthenticationFilterTest.java @@ -525,7 +525,7 @@ private static Filter.FilterConfigParseContext getFilterContext() { .node(Node.newBuilder().build()) .build()) .serverInfo(ServerInfo.create( - "test_target", Collections.emptyMap(), false, true, false, false, null)) + "test_target", Collections.emptyMap(), false, true, false, false)) .build(); } } diff --git a/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java b/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java index c5cbd1c5442..d4ee4159bc2 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcBootstrapperImplTest.java @@ -24,10 +24,8 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; -import io.grpc.CallCredentials; import io.grpc.InsecureChannelCredentials; import io.grpc.TlsChannelCredentials; -import io.grpc.auth.JwtTokenFileCallCredentials; import io.grpc.internal.GrpcUtil; import io.grpc.internal.GrpcUtil.GrpcBuildVersion; import io.grpc.xds.client.AllowedGrpcServices; @@ -1060,258 +1058,4 @@ private static Node.Builder getNodeBuilder() { .addClientFeatures(GrpcBootstrapperImpl.CLIENT_FEATURE_DISABLE_OVERPROVISIONING) .addClientFeatures(GrpcBootstrapperImpl.CLIENT_FEATURE_RESOURCE_IN_SOTW); } - - private static void setEnableXdsBootstrapCallCreds(boolean enable) { - io.grpc.xds.client.BootstrapperImpl.enableXdsBootstrapCallCreds = enable; - } - - private static String getFilePath(JwtTokenFileCallCredentials credentials) { - try { - java.lang.reflect.Field field = - JwtTokenFileCallCredentials.class.getDeclaredField("filePath"); - field.setAccessible(true); - return (String) field.get(credentials); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Test - public void parseBootstrap_callCreds_flagDisabled() throws Exception { - setEnableXdsBootstrapCallCreds(false); - try { - String rawData = "{\n" - + " \"xds_servers\": [\n" - + " {\n" - + " \"server_uri\": \"" + SERVER_URI + "\",\n" - + " \"channel_creds\": [{\"type\": \"insecure\"}],\n" - + " \"call_creds\": [\n" - + " {\n" - + " \"type\": \"jwt_token_file\",\n" - + " \"config\": {\n" - + " \"jwt_token_file\": \"/var/run/secrets/token\"\n" - + " }\n" - + " }\n" - + " ]\n" - + " }\n" - + " ]\n" - + "}"; - bootstrapper.setFileReader(createFileReader(BOOTSTRAP_FILE_PATH, rawData)); - BootstrapInfo info = bootstrapper.bootstrap(); - assertThat(info.servers()).hasSize(1); - ServerInfo serverInfo = Iterables.getOnlyElement(info.servers()); - assertThat(serverInfo.callCredentials()).isNull(); - } finally { - setEnableXdsBootstrapCallCreds(false); - } - } - - @Test - public void parseBootstrap_xdsServers_jwtTokenFileCallCreds() throws Exception { - setEnableXdsBootstrapCallCreds(true); - try { - String rawData = "{\n" - + " \"xds_servers\": [\n" - + " {\n" - + " \"server_uri\": \"" + SERVER_URI + "\",\n" - + " \"channel_creds\": [{\"type\": \"insecure\"}],\n" - + " \"call_creds\": [\n" - + " {\n" - + " \"type\": \"jwt_token_file\",\n" - + " \"config\": {\n" - + " \"jwt_token_file\": \"/var/run/secrets/token\"\n" - + " }\n" - + " }\n" - + " ]\n" - + " }\n" - + " ]\n" - + "}"; - bootstrapper.setFileReader(createFileReader(BOOTSTRAP_FILE_PATH, rawData)); - BootstrapInfo info = bootstrapper.bootstrap(); - assertThat(info.servers()).hasSize(1); - ServerInfo serverInfo = Iterables.getOnlyElement(info.servers()); - assertThat(serverInfo.callCredentials()) - .isInstanceOf(JwtTokenFileCallCredentials.class); - assertThat(getFilePath((JwtTokenFileCallCredentials) serverInfo.callCredentials())) - .isEqualTo("/var/run/secrets/token"); - } finally { - setEnableXdsBootstrapCallCreds(false); - } - } - - @Test - public void parseBootstrap_authorities_jwtTokenFileCallCreds() throws Exception { - setEnableXdsBootstrapCallCreds(true); - try { - String rawData = "{\n" - + " \"authorities\": {\n" - + " \"a.com\": {\n" - + " \"xds_servers\": [\n" - + " {\n" - + " \"server_uri\": \"td2.googleapis.com:443\",\n" - + " \"channel_creds\": [\n" - + " {\"type\": \"insecure\"}\n" - + " ],\n" - + " \"call_creds\": [\n" - + " {\n" - + " \"type\": \"jwt_token_file\",\n" - + " \"config\": {\n" - + " \"jwt_token_file\": \"/var/run/secrets/authority_token\"\n" - + " }\n" - + " }\n" - + " ]\n" - + " }\n" - + " ]\n" - + " }\n" - + " },\n" - + " \"xds_servers\": [\n" - + " {\n" - + " \"server_uri\": \"" + SERVER_URI + "\",\n" - + " \"channel_creds\": [\n" - + " {\"type\": \"insecure\"}\n" - + " ]\n" - + " }\n" - + " ]\n" - + "}"; - bootstrapper.setFileReader(createFileReader(BOOTSTRAP_FILE_PATH, rawData)); - BootstrapInfo info = bootstrapper.bootstrap(); - assertThat(info.authorities()).hasSize(1); - AuthorityInfo authorityInfo = info.authorities().get("a.com"); - assertThat(authorityInfo.xdsServers()).hasSize(1); - ServerInfo serverInfo = authorityInfo.xdsServers().get(0); - assertThat(serverInfo.callCredentials()) - .isInstanceOf(JwtTokenFileCallCredentials.class); - assertThat(getFilePath((JwtTokenFileCallCredentials) serverInfo.callCredentials())) - .isEqualTo("/var/run/secrets/authority_token"); - } finally { - setEnableXdsBootstrapCallCreds(false); - } - } - - @Test - public void parseBootstrap_unsupportedCallCredsType_ignored() throws Exception { - setEnableXdsBootstrapCallCreds(true); - try { - String rawData = "{\n" - + " \"xds_servers\": [\n" - + " {\n" - + " \"server_uri\": \"" + SERVER_URI + "\",\n" - + " \"channel_creds\": [{\"type\": \"insecure\"}],\n" - + " \"call_creds\": [\n" - + " {\n" - + " \"type\": \"unsupported_type\",\n" - + " \"config\": {\n" - + " \"some_field\": \"some_val\"\n" - + " }\n" - + " },\n" - + " {\n" - + " \"type\": \"jwt_token_file\",\n" - + " \"config\": {\n" - + " \"jwt_token_file\": \"/var/run/secrets/token\"\n" - + " }\n" - + " }\n" - + " ]\n" - + " }\n" - + " ]\n" - + "}"; - bootstrapper.setFileReader(createFileReader(BOOTSTRAP_FILE_PATH, rawData)); - BootstrapInfo info = bootstrapper.bootstrap(); - assertThat(info.servers()).hasSize(1); - ServerInfo serverInfo = Iterables.getOnlyElement(info.servers()); - assertThat(serverInfo.callCredentials()) - .isInstanceOf(JwtTokenFileCallCredentials.class); - assertThat(getFilePath((JwtTokenFileCallCredentials) serverInfo.callCredentials())) - .isEqualTo("/var/run/secrets/token"); - } finally { - setEnableXdsBootstrapCallCreds(false); - } - } - - @Test - public void parseBootstrap_malformedCallCreds_throws() throws Exception { - setEnableXdsBootstrapCallCreds(true); - try { - String rawData = "{\n" - + " \"xds_servers\": [\n" - + " {\n" - + " \"server_uri\": \"" + SERVER_URI + "\",\n" - + " \"channel_creds\": [{\"type\": \"insecure\"}],\n" - + " \"call_creds\": [\n" - + " {\n" - + " \"type\": \"jwt_token_file\",\n" - + " \"config\": {}\n" - + " }\n" - + " ]\n" - + " }\n" - + " ]\n" - + "}"; - bootstrapper.setFileReader(createFileReader(BOOTSTRAP_FILE_PATH, rawData)); - XdsInitializationException e = assertThrows(XdsInitializationException.class, - bootstrapper::bootstrap); - assertThat(e).hasMessageThat().contains("jwt_token_file' jwt_token_file missing or empty"); - } finally { - setEnableXdsBootstrapCallCreds(false); - } - } - - @Test - public void parseBootstrap_xdsServers_multipleValidCallCreds() throws Exception { - setEnableXdsBootstrapCallCreds(true); - try { - String rawData = "{\n" - + " \"xds_servers\": [\n" - + " {\n" - + " \"server_uri\": \"" + SERVER_URI + "\",\n" - + " \"channel_creds\": [{\"type\": \"insecure\"}],\n" - + " \"call_creds\": [\n" - + " {\n" - + " \"type\": \"jwt_token_file\",\n" - + " \"config\": { \"jwt_token_file\": \"/var/run/secrets/token1\" }\n" - + " },\n" - + " {\n" - + " \"type\": \"jwt_token_file\",\n" - + " \"config\": { \"jwt_token_file\": \"/var/run/secrets/token2\" }\n" - + " }\n" - + " ]\n" - + " }\n" - + " ]\n" - + "}"; - bootstrapper.setFileReader(createFileReader(BOOTSTRAP_FILE_PATH, rawData)); - BootstrapInfo info = bootstrapper.bootstrap(); - assertThat(info.servers()).hasSize(1); - ServerInfo serverInfo = info.servers().get(0); - CallCredentials creds = serverInfo.callCredentials(); - assertThat(creds).isNotNull(); - assertThat(creds).isInstanceOf(io.grpc.CompositeCallCredentials.class); - } finally { - setEnableXdsBootstrapCallCreds(false); - } - } - - @Test - public void parseBootstrap_xdsServers_missingTypeCallCreds() throws Exception { - setEnableXdsBootstrapCallCreds(true); - try { - String rawData = "{\n" - + " \"xds_servers\": [\n" - + " {\n" - + " \"server_uri\": \"" + SERVER_URI + "\",\n" - + " \"channel_creds\": [{\"type\": \"insecure\"}],\n" - + " \"call_creds\": [\n" - + " {\n" - + " \"config\": { \"jwt_token_file\": \"/var/run/secrets/token\" }\n" - + " }\n" - + " ]\n" - + " }\n" - + " ]\n" - + "}"; - bootstrapper.setFileReader(createFileReader(BOOTSTRAP_FILE_PATH, rawData)); - bootstrapper.bootstrap(); - fail("Expected exception"); - } catch (XdsInitializationException e) { - assertThat(e).hasMessageThat().contains("with 'call_creds' type unspecified"); - } finally { - setEnableXdsBootstrapCallCreds(false); - } - } } diff --git a/xds/src/test/java/io/grpc/xds/GrpcServiceConfigParserTest.java b/xds/src/test/java/io/grpc/xds/GrpcServiceConfigParserTest.java index 56b153502f1..7dd4bb7cb67 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcServiceConfigParserTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcServiceConfigParserTest.java @@ -80,7 +80,7 @@ private static ServerInfo dummyServerInfo() { private static ServerInfo dummyServerInfo(boolean isTrusted) { return ServerInfo.create("test_target", Collections.emptyMap(), false, isTrusted, false, - false, null); + false); } private static GrpcServiceConfig parse( diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java index bb13d155843..dc9590aed51 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplDataTest.java @@ -3628,7 +3628,7 @@ private static Filter buildHttpConnectionManagerFilter(HttpFilter... httpFilters private XdsResourceType.Args getXdsResourceTypeArgs(boolean isTrustedServer) { return new XdsResourceType.Args( - ServerInfo.create("http://td", "", false, isTrustedServer, false, false, null), "1.0", null, XdsTestUtils.EMPTY_BOOTSTRAP, null, null + ServerInfo.create("http://td", "", false, isTrustedServer, false, false), "1.0", null, XdsTestUtils.EMPTY_BOOTSTRAP, null, null ); } } diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java index 9381aab2e76..60bb9ab8da2 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java @@ -366,7 +366,7 @@ public void setUp() throws IOException { cleanupRule.register(InProcessChannelBuilder.forName(serverName).directExecutor().build()); xdsServerInfo = ServerInfo.create(SERVER_URI, CHANNEL_CREDENTIALS, ignoreResourceDeletion(), - true, false, false, null); + true, false, false); BootstrapInfo bootstrapInfo = Bootstrapper.BootstrapInfo.builder() .servers(Collections.singletonList(xdsServerInfo)) @@ -1557,7 +1557,7 @@ public void ldsResourceDeleted_ignoreResourceDeletion() { public void ldsResourceDeleted_failOnDataErrors_true() { BootstrapperImpl.xdsDataErrorHandlingEnabled = true; xdsServerInfo = ServerInfo.create(SERVER_URI, CHANNEL_CREDENTIALS, false, - true, false, true, null); + true, false, true); BootstrapInfo bootstrapInfo = Bootstrapper.BootstrapInfo.builder() .servers(Collections.singletonList(xdsServerInfo)) @@ -1618,7 +1618,7 @@ public void ldsResourceDeleted_failOnDataErrors_false() { BootstrapperImpl.xdsDataErrorHandlingEnabled = true; xdsServerInfo = ServerInfo.create(SERVER_URI, CHANNEL_CREDENTIALS, false, - true, false, false, null); + true, false, false); BootstrapInfo bootstrapInfo = Bootstrapper.BootstrapInfo.builder() .servers(Collections.singletonList(xdsServerInfo)) @@ -1681,7 +1681,7 @@ public void ldsResourceDeleted_failOnDataErrorsIgnoredWithoutEnvVar() { BootstrapperImpl.xdsDataErrorHandlingEnabled = false; xdsServerInfo = ServerInfo.create(SERVER_URI, CHANNEL_CREDENTIALS, false, - true, false, true, null); + true, false, true); BootstrapInfo bootstrapInfo = Bootstrapper.BootstrapInfo.builder() .servers(Collections.singletonList(xdsServerInfo)) @@ -3244,7 +3244,7 @@ public void cdsResourceDeleted_ignoreResourceDeletion() { public void cdsResourceDeleted_failOnDataErrors_true() { BootstrapperImpl.xdsDataErrorHandlingEnabled = true; xdsServerInfo = ServerInfo.create(SERVER_URI, CHANNEL_CREDENTIALS, false, - true, false, true, null); + true, false, true); BootstrapInfo bootstrapInfo = Bootstrapper.BootstrapInfo.builder() .servers(Collections.singletonList(xdsServerInfo)) @@ -3303,7 +3303,7 @@ public void cdsResourceDeleted_failOnDataErrors_false() { BootstrapperImpl.xdsDataErrorHandlingEnabled = true; // Set failOnDataErrors to false for this test case. xdsServerInfo = ServerInfo.create(SERVER_URI, CHANNEL_CREDENTIALS, false, - true, false, false, null); + true, false, false); BootstrapInfo bootstrapInfo = Bootstrapper.BootstrapInfo.builder() .servers(Collections.singletonList(xdsServerInfo)) @@ -3370,7 +3370,7 @@ public void cdsResourceDeleted_failOnDataErrors_false() { public void ldsResourceNacked_withFailOnDataErrors_dropsResource() { BootstrapperImpl.xdsDataErrorHandlingEnabled = true; xdsServerInfo = ServerInfo.create(SERVER_URI, CHANNEL_CREDENTIALS, false, - true, false, true, null); + true, false, true); BootstrapInfo bootstrapInfo = Bootstrapper.BootstrapInfo.builder() .servers(Collections.singletonList(xdsServerInfo)) @@ -3419,7 +3419,7 @@ public void ldsResourceNacked_withFailOnDataErrors_dropsResource() { public void ldsResourceNacked_withFailOnDataErrorsDisabled_isAmbientError() { BootstrapperImpl.xdsDataErrorHandlingEnabled = true; xdsServerInfo = ServerInfo.create(SERVER_URI, CHANNEL_CREDENTIALS, false, - true, false, false, null); + true, false, false); BootstrapInfo bootstrapInfo = Bootstrapper.BootstrapInfo.builder() .servers(Collections.singletonList(xdsServerInfo)) @@ -3852,7 +3852,7 @@ public void flowControlAbsent() throws Exception { public void resourceTimerIsTransientError_schedulesExtendedTimeout() { BootstrapperImpl.xdsDataErrorHandlingEnabled = true; ServerInfo serverInfo = ServerInfo.create(SERVER_URI, CHANNEL_CREDENTIALS, - false, true, true, false, null); + false, true, true, false); BootstrapInfo bootstrapInfo = Bootstrapper.BootstrapInfo.builder() .servers(Collections.singletonList(serverInfo)) @@ -3897,7 +3897,7 @@ public void resourceTimerIsTransientError_schedulesExtendedTimeout() { public void resourceTimerIsTransientError_callsOnErrorUnavailable() { BootstrapperImpl.xdsDataErrorHandlingEnabled = true; xdsServerInfo = ServerInfo.create(SERVER_URI, CHANNEL_CREDENTIALS, ignoreResourceDeletion(), - true, true, false, null); + true, true, false); BootstrapInfo bootstrapInfo = Bootstrapper.BootstrapInfo.builder() .servers(Collections.singletonList(xdsServerInfo)) @@ -5127,7 +5127,7 @@ private XdsClientImpl createXdsClient(String serverUri) { private BootstrapInfo buildBootStrap(String serverUri) { ServerInfo xdsServerInfo = ServerInfo.create(serverUri, CHANNEL_CREDENTIALS, - ignoreResourceDeletion(), true, false, false, null); + ignoreResourceDeletion(), true, false, false); return Bootstrapper.BootstrapInfo.builder() .servers(Collections.singletonList(xdsServerInfo)) diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsTransportFactoryTest.java b/xds/src/test/java/io/grpc/xds/GrpcXdsTransportFactoryTest.java index 484b081915b..d6c3c6ea69e 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsTransportFactoryTest.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsTransportFactoryTest.java @@ -27,13 +27,11 @@ import io.envoyproxy.envoy.service.discovery.v3.DiscoveryRequest; import io.envoyproxy.envoy.service.discovery.v3.DiscoveryResponse; import io.grpc.BindableService; -import io.grpc.CallCredentials; import io.grpc.CallOptions; import io.grpc.Channel; import io.grpc.ChannelConfigurator; import io.grpc.ClientCall; import io.grpc.ClientInterceptor; -import io.grpc.CompositeCallCredentials; import io.grpc.Grpc; import io.grpc.InsecureChannelCredentials; import io.grpc.InsecureServerCredentials; @@ -341,66 +339,5 @@ public void configureChannelBuilder(ManagedChannelBuilder builder) { verify(mockBuilder).intercept(interceptor1); verify(mockBuilder).intercept(interceptor2); } - - private static CallCredentials getCallCredentials( - XdsTransportFactory.XdsTransport transport) throws Exception { - java.lang.reflect.Field field = - GrpcXdsTransportFactory.GrpcXdsTransport.class - .getDeclaredField("callCredentials"); - field.setAccessible(true); - return (CallCredentials) field.get(transport); - } - - private static CallCredentials getCredentials1( - CompositeCallCredentials composite) throws Exception { - java.lang.reflect.Field field = - CompositeCallCredentials.class.getDeclaredField("credentials1"); - field.setAccessible(true); - return (CallCredentials) field.get(composite); - } - - private static CallCredentials getCredentials2( - CompositeCallCredentials composite) throws Exception { - java.lang.reflect.Field field = - CompositeCallCredentials.class.getDeclaredField("credentials2"); - field.setAccessible(true); - return (CallCredentials) field.get(composite); - } - - @Test - public void createTransport_combinesCallCredentials() throws Exception { - CallCredentials factoryCreds = mock(CallCredentials.class); - CallCredentials serverCreds = mock(CallCredentials.class); - - // 1. Both factory and server callCredentials are non-null - GrpcXdsTransportFactory factoryBoth = new GrpcXdsTransportFactory(factoryCreds, null); - Bootstrapper.ServerInfo serverInfoBoth = Bootstrapper.ServerInfo.create( - "localhost:8080", InsecureChannelCredentials.create(), - false, false, false, false, serverCreds); - XdsTransportFactory.XdsTransport transportBoth = factoryBoth.create(serverInfoBoth); - CallCredentials combined = getCallCredentials(transportBoth); - assertThat(combined).isInstanceOf(CompositeCallCredentials.class); - CompositeCallCredentials composite = (CompositeCallCredentials) combined; - assertThat(getCredentials1(composite)).isSameInstanceAs(factoryCreds); - assertThat(getCredentials2(composite)).isSameInstanceAs(serverCreds); - transportBoth.shutdown(); - - // 2. Server credentials are null -> resolves to factory credentials - GrpcXdsTransportFactory factoryOnly = new GrpcXdsTransportFactory(factoryCreds, null); - Bootstrapper.ServerInfo serverInfoNoCreds = Bootstrapper.ServerInfo.create( - "localhost:8080", InsecureChannelCredentials.create()); - XdsTransportFactory.XdsTransport transportFactoryOnly = factoryOnly.create(serverInfoNoCreds); - assertThat(getCallCredentials(transportFactoryOnly)).isSameInstanceAs(factoryCreds); - transportFactoryOnly.shutdown(); - - // 3. Factory credentials are null -> resolves to server credentials - GrpcXdsTransportFactory factoryNone = new GrpcXdsTransportFactory(null, null); - Bootstrapper.ServerInfo serverInfoWithCreds = Bootstrapper.ServerInfo.create( - "localhost:8080", InsecureChannelCredentials.create(), - false, false, false, false, serverCreds); - XdsTransportFactory.XdsTransport transportServerOnly = factoryNone.create(serverInfoWithCreds); - assertThat(getCallCredentials(transportServerOnly)).isSameInstanceAs(serverCreds); - transportServerOnly.shutdown(); - } } diff --git a/xds/src/test/java/io/grpc/xds/RbacFilterTest.java b/xds/src/test/java/io/grpc/xds/RbacFilterTest.java index 18074dfabf8..4680f570327 100644 --- a/xds/src/test/java/io/grpc/xds/RbacFilterTest.java +++ b/xds/src/test/java/io/grpc/xds/RbacFilterTest.java @@ -483,7 +483,7 @@ private Filter.FilterConfigParseContext getFilterContext() { .node(Node.newBuilder().build()) .build()) .serverInfo(ServerInfo.create( - "test_target", Collections.emptyMap(), false, true, false, false, null)) + "test_target", Collections.emptyMap(), false, true, false, false)) .build(); } } diff --git a/xds/src/test/java/io/grpc/xds/XdsJwtCallCredsIntegrationTest.java b/xds/src/test/java/io/grpc/xds/XdsJwtCallCredsIntegrationTest.java deleted file mode 100644 index 1c3ab37c7c0..00000000000 --- a/xds/src/test/java/io/grpc/xds/XdsJwtCallCredsIntegrationTest.java +++ /dev/null @@ -1,244 +0,0 @@ -/* - * Copyright 2026 The gRPC Authors - * - * 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 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.grpc.xds; - -import static com.google.common.truth.Truth.assertThat; - -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Lists; -import io.grpc.ChannelConfigurator; -import io.grpc.Grpc; -import io.grpc.InsecureChannelCredentials; -import io.grpc.ManagedChannel; -import io.grpc.ManagedChannelBuilder; -import io.grpc.Metadata; -import io.grpc.NameResolverRegistry; -import io.grpc.Server; -import io.grpc.ServerCall; -import io.grpc.ServerCallHandler; -import io.grpc.ServerCredentials; -import io.grpc.ServerInterceptor; -import io.grpc.ServerInterceptors; -import io.grpc.TlsServerCredentials; -import io.grpc.internal.testing.TestUtils; -import io.grpc.testing.TlsTesting; -import io.grpc.testing.protobuf.SimpleRequest; -import io.grpc.testing.protobuf.SimpleServiceGrpc; -import java.io.File; -import java.io.FileOutputStream; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.security.KeyStore; -import java.security.cert.CertificateFactory; -import java.util.Collections; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; -import javax.net.ssl.TrustManagerFactory; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -/** - * Integration test for verifying that when the xDS client connects to a fake xDS control plane, the - * request contains the Authorization header with a JWT token configured via the bootstrap config. - */ -@RunWith(JUnit4.class) -public class XdsJwtCallCredsIntegrationTest { - - private Path trustStorePath; - private Path jwtTokenFile; - private String jwtToken; - private Server server; - private XdsTestControlPlaneService controlPlaneService; - private XdsNameResolverProvider nameResolverProvider; - private ManagedChannel channel; - private final AtomicReference receivedAuthHeader = new AtomicReference<>(); - private final CountDownLatch authHeaderLatch = new CountDownLatch(1); - - @Before - public void setUp() throws Exception { - setEnableXdsBootstrapCallCreds(true); - - // Client-side TLS trust setup - trustStorePath = generateTrustStore(); - System.setProperty("javax.net.ssl.trustStore", trustStorePath.toAbsolutePath().toString()); - System.setProperty("javax.net.ssl.trustStorePassword", "changeit"); - System.setProperty("javax.net.ssl.trustStoreType", "JKS"); - createDefaultTrustManager(); - - // Create JWT token file - jwtTokenFile = Files.createTempFile("jwt-token", ".txt"); - jwtToken = generateJwtToken(); - Files.write(jwtTokenFile, jwtToken.getBytes(StandardCharsets.UTF_8)); - } - - @After - public void tearDown() throws Exception { - if (channel != null) { - channel.shutdownNow(); - channel.awaitTermination(5, TimeUnit.SECONDS); - } - if (server != null) { - server.shutdownNow(); - server.awaitTermination(5, TimeUnit.SECONDS); - } - if (nameResolverProvider != null) { - NameResolverRegistry.getDefaultRegistry().deregister(nameResolverProvider); - } - - System.clearProperty("javax.net.ssl.trustStore"); - System.clearProperty("javax.net.ssl.trustStorePassword"); - System.clearProperty("javax.net.ssl.trustStoreType"); - createDefaultTrustManager(); - - if (trustStorePath != null) { - Files.deleteIfExists(trustStorePath); - } - if (jwtTokenFile != null) { - Files.deleteIfExists(jwtTokenFile); - } - setEnableXdsBootstrapCallCreds(false); - } - - @Test - public void jwtCallCredsAppliedToXdsControlPlane() throws Exception { - controlPlaneService = new XdsTestControlPlaneService(); - - File certFile = TestUtils.loadCert("server1.pem"); - File keyFile = TestUtils.loadCert("server1.key"); - ServerCredentials serverCreds = TlsServerCredentials.newBuilder() - .keyManager(certFile, keyFile) - .build(); - - ServerInterceptor authCheckingInterceptor = new ServerInterceptor() { - @Override - public ServerCall.Listener interceptCall( - ServerCall call, Metadata headers, ServerCallHandler next) { - String authHeader = headers.get( - Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER)); - if (authHeader != null) { - receivedAuthHeader.set(authHeader); - authHeaderLatch.countDown(); - } - return next.startCall(call, headers); - } - }; - - server = Grpc.newServerBuilderForPort(0, serverCreds) - .addService(ServerInterceptors.intercept(controlPlaneService, authCheckingInterceptor)) - .build() - .start(); - - // Setup bootstrap configuration with call_creds pointing to the JWT token file. - Map bootstrapOverride = ImmutableMap.of( - "node", ImmutableMap.of( - "id", UUID.randomUUID().toString(), - "cluster", "cluster0"), - "xds_servers", Collections.singletonList( - ImmutableMap.of( - "server_uri", "localhost:" + server.getPort(), - "channel_creds", Collections.singletonList( - ImmutableMap.of("type", "tls") - ), - "call_creds", Collections.singletonList( - ImmutableMap.of( - "type", "jwt_token_file", - "config", ImmutableMap.of( - "jwt_token_file", jwtTokenFile.toAbsolutePath().toString()) - ) - ), - "server_features", Lists.newArrayList("xds_v3") - ) - ), - "server_listener_resource_name_template", "grpc/server?udpa.resource.listening_address=" - ); - - // Register name resolver - nameResolverProvider = XdsNameResolverProvider.createForTest("test-xds", bootstrapOverride); - NameResolverRegistry.getDefaultRegistry().register(nameResolverProvider); - - // Create channel and make a dummy RPC call to trigger name resolver and connection - channel = Grpc.newChannelBuilder("test-xds:///test-server", InsecureChannelCredentials.create()) - .childChannelConfigurator(new ChannelConfigurator() { - @Override - public void configureChannelBuilder(ManagedChannelBuilder builder) { - builder.overrideAuthority("waterzooi.test.google.be"); - } - }) - .build(); - SimpleServiceGrpc.SimpleServiceBlockingStub blockingStub = - SimpleServiceGrpc.newBlockingStub(channel); - - try { - blockingStub.unaryRpc(SimpleRequest.getDefaultInstance()); - } catch (Exception e) { - // Expected to fail since control plane doesn't actually serve the LDS/RDS configs for - // test-server, but the connection/stream to control plane should still happen. - } - - // Verify control plane received the token in Authorization header - assertThat(authHeaderLatch.await(10, TimeUnit.SECONDS)).isTrue(); - assertThat(receivedAuthHeader.get()).isEqualTo("Bearer " + jwtToken); - } - - private static void setEnableXdsBootstrapCallCreds(boolean enable) { - io.grpc.xds.client.BootstrapperImpl.enableXdsBootstrapCallCreds = enable; - } - - private static Path generateTrustStore() throws Exception { - KeyStore keystore = KeyStore.getInstance("JKS"); - keystore.load(null, null); - try (InputStream caCertStream = TlsTesting.loadCert("ca.pem")) { - keystore.setCertificateEntry("testca", - CertificateFactory.getInstance("X.509").generateCertificate(caCertStream)); - } - File trustStoreFile = File.createTempFile("testca-truststore", ".jks"); - trustStoreFile.deleteOnExit(); - try (FileOutputStream out = new FileOutputStream(trustStoreFile)) { - keystore.store(out, "changeit".toCharArray()); - } - return trustStoreFile.toPath(); - } - - private static void createDefaultTrustManager() throws Exception { - TrustManagerFactory factory = - TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - factory.init((KeyStore) null); - } - - private static String generateJwtToken() { - String header = "{\"alg\":\"none\",\"typ\":\"JWT\"}"; - String payload = "{\"exp\":2000000000}"; - String signature = ""; - - String headerBase64 = com.google.common.io.BaseEncoding.base64Url().omitPadding() - .encode(header.getBytes(StandardCharsets.UTF_8)); - String payloadBase64 = com.google.common.io.BaseEncoding.base64Url().omitPadding() - .encode(payload.getBytes(StandardCharsets.UTF_8)); - String signatureBase64 = com.google.common.io.BaseEncoding.base64Url().omitPadding() - .encode(signature.getBytes(StandardCharsets.UTF_8)); - - return headerBase64 + "." + payloadBase64 + "." + signatureBase64; - } -} diff --git a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java index 1a92598aaf3..3343bd4de51 100644 --- a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java @@ -400,15 +400,14 @@ public void resolving_targetAuthorityInAuthoritiesMap() { String serviceAuthority = "[::FFFF:129.144.52.38]:80"; bootstrapInfo = BootstrapInfo.builder() .servers(ImmutableList.of(ServerInfo.create( - "td.googleapis.com", InsecureChannelCredentials.create(), true, true, false, false, - null))) + "td.googleapis.com", InsecureChannelCredentials.create(), true, true, false, false))) .node(Node.newBuilder().build()) .authorities( ImmutableMap.of(targetAuthority, AuthorityInfo.create( "xdstp://" + targetAuthority + "/envoy.config.listener.v3.Listener/%s?foo=1&bar=2", ImmutableList.of(ServerInfo.create( "td.googleapis.com", InsecureChannelCredentials.create(), - true, true, false, false, null))))) + true, true, false, false))))) .build(); expectedLdsResourceName = "xdstp://xds.authority.com/envoy.config.listener.v3.Listener/" + "%5B::FFFF:129.144.52.38%5D:80?bar=2&foo=1"; // query param canonified diff --git a/xds/src/test/java/io/grpc/xds/XdsTestUtils.java b/xds/src/test/java/io/grpc/xds/XdsTestUtils.java index 8229c5fef94..a0ebd0ea1bf 100644 --- a/xds/src/test/java/io/grpc/xds/XdsTestUtils.java +++ b/xds/src/test/java/io/grpc/xds/XdsTestUtils.java @@ -86,7 +86,7 @@ public class XdsTestUtils { + ".HttpConnectionManager"; static final Bootstrapper.ServerInfo EMPTY_BOOTSTRAPPER_SERVER_INFO = Bootstrapper.ServerInfo.create( - "td.googleapis.com", InsecureChannelCredentials.create(), false, true, false, false, null); + "td.googleapis.com", InsecureChannelCredentials.create(), false, true, false, false); static final Bootstrapper.BootstrapInfo EMPTY_BOOTSTRAP = Bootstrapper.BootstrapInfo.builder() .servers(com.google.common.collect.ImmutableList.of(EMPTY_BOOTSTRAPPER_SERVER_INFO)) diff --git a/xds/src/test/java/io/grpc/xds/client/CommonBootstrapperTestUtils.java b/xds/src/test/java/io/grpc/xds/client/CommonBootstrapperTestUtils.java index 38df2ec74cb..e3760bd983f 100644 --- a/xds/src/test/java/io/grpc/xds/client/CommonBootstrapperTestUtils.java +++ b/xds/src/test/java/io/grpc/xds/client/CommonBootstrapperTestUtils.java @@ -203,7 +203,7 @@ public static Bootstrapper.BootstrapInfo buildBootStrap(List serverUris) List serverInfos = new ArrayList<>(); for (String uri : serverUris) { - serverInfos.add(ServerInfo.create(uri, CHANNEL_CREDENTIALS, false, true, false, false, null)); + serverInfos.add(ServerInfo.create(uri, CHANNEL_CREDENTIALS, false, true, false, false)); } EnvoyProtoData.Node node = EnvoyProtoData.Node.newBuilder().setId("node-id").build();