Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ RUN cat /java-formatter-version
RUN V=$(cat /java-formatter-version) && curl -o "/google-java-format.jar" "https://maven-central.storage-download.googleapis.com/maven2/com/google/googlejavaformat/google-java-format/${V}/google-java-format-${V}-all-deps.jar"

# Compile and install packages
RUN mvn install -B -ntp -DskipTests -Dclirr.skip -Dcheckstyle.skip
RUN mvn install -B -ntp -T 1.5C -DskipTests -Dcheckstyle.skip -Dclirr.skip -Denforcer.skip -Dfmt.skip
RUN cp "/root/.m2/repository/com/google/api/gapic-generator-java/${DOCKER_GAPIC_GENERATOR_VERSION}/gapic-generator-java-${DOCKER_GAPIC_GENERATOR_VERSION}.jar" \
"./gapic-generator-java.jar"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,14 @@ static GapicMetadata defaultGapicMetadata() {

public abstract Transport transport();

public abstract Optional<String> repo();

public static Builder builder() {
return new AutoValue_GapicContext.Builder()
.setMixinServices(Collections.emptyList())
.setGapicMetadataEnabled(false)
.setRestNumericEnumsEnabled(false);
.setRestNumericEnumsEnabled(false)
.setRepo(Optional.empty());
}

@AutoValue.Builder
Expand Down Expand Up @@ -130,6 +133,8 @@ public Builder setHelperResourceNames(Set<ResourceName> helperResourceNames) {

public abstract Builder setTransport(Transport transport);

public abstract Builder setRepo(Optional<String> repo);

abstract ImmutableMap<String, ResourceName> resourceNames();

abstract ImmutableMap<String, ResourceName> helperResourceNames();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ public static GapicContext parse(CodeGeneratorRequest request) {
Optional<GapicLanguageSettings> languageSettingsOpt =
GapicLanguageSettingsParser.parse(gapicYamlConfigPathOpt);
Optional<String> transportOpt = PluginArgumentParser.parseTransport(request);
Optional<String> repoOpt = PluginArgumentParser.parseRepo(request);

boolean willGenerateMetadata = PluginArgumentParser.hasMetadataFlag(request);
boolean willGenerateNumericEnum = PluginArgumentParser.hasNumericEnumFlag(request);
Expand Down Expand Up @@ -253,6 +254,7 @@ public static GapicContext parse(CodeGeneratorRequest request) {
.setServiceYamlProto(serviceYamlProtoOpt.orElse(null))
.setTransport(transport)
.setRestNumericEnumsEnabled(willGenerateNumericEnum)
.setRepo(repoOpt)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public class PluginArgumentParser {
@VisibleForTesting static final String KEY_NUMERIC_ENUM = "rest-numeric-enums";
@VisibleForTesting static final String KEY_SERVICE_YAML_CONFIG = "api-service-config";
@VisibleForTesting static final String KEY_TRANSPORT = "transport";
@VisibleForTesting static final String KEY_REPO = "repo";

private static final String JSON_FILE_ENDING = "grpc_service_config.json";
private static final String GAPIC_YAML_FILE_ENDING = "gapic.yaml";
Expand All @@ -53,6 +54,10 @@ static Optional<String> parseTransport(CodeGeneratorRequest request) {
return parseConfigArgument(request.getParameter(), KEY_TRANSPORT);
}

static Optional<String> parseRepo(CodeGeneratorRequest request) {
return parseConfigArgument(request.getParameter(), KEY_REPO);
}
Comment on lines +57 to +59
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The parseRepo method uses parseConfigArgument, which parses plugin arguments by splitting the input string by commas. If the repo value contains a comma, it will be split, potentially allowing an attacker to inject additional plugin flags or options (e.g., injecting ,metadata to enable metadata generation).


static boolean hasMetadataFlag(CodeGeneratorRequest request) {
return hasFlag(request.getParameter(), KEY_METADATA);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ protected static CodeGeneratorResponse write(

writeMetadataFile(context, writePackageInfo(gapicPackageInfo, codeWriter, jos), jos);
writeReflectConfigFile(gapicPackageInfo.packageInfo().pakkage(), reflectConfigInfo, jos);
writeGapicPropertiesFile(context, jos);

jos.finish();
jos.flush();
Expand Down Expand Up @@ -212,6 +213,22 @@ private static void writeMetadataFile(GapicContext context, String path, JarOutp
}
}

@VisibleForTesting
static void writeGapicPropertiesFile(GapicContext context, JarOutputStream jos) {
context
.repo()
.ifPresent(
repo -> {
JarEntry jarEntry = new JarEntry("src/main/resources/gapic.properties");
try {
jos.putNextEntry(jarEntry);
jos.write(String.format("repo=%s\n", repo).getBytes(StandardCharsets.UTF_8));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The writeGapicPropertiesFile method writes the repo string directly into a properties file using String.format("repo=%s\n", repo). If the repo string (which originates from user-supplied plugin arguments) contains a newline character, it can be used to inject arbitrary properties into the gapic.properties file. This could potentially influence the behavior of other components that read this configuration file.

} catch (IOException e) {
throw new GapicWriterException("Could not write repo file", e);
}
});
}

private static String getPath(String pakkage, String className) {
String path = pakkage.replaceAll("\\.", "/");
if (className.startsWith("Mock") || className.endsWith("Test")) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import static com.google.api.generator.gapic.protoparser.PluginArgumentParser.KEY_METADATA;
import static com.google.api.generator.gapic.protoparser.PluginArgumentParser.KEY_NUMERIC_ENUM;
import static com.google.api.generator.gapic.protoparser.PluginArgumentParser.KEY_REPO;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
Expand Down Expand Up @@ -269,6 +270,21 @@ void hasFlag_flagFound() {
assertTrue(PluginArgumentParser.hasFlag(rawArgument, KEY_METADATA));
}

@Test
void parseRepo_onlyOnePresent() {
String repo = "googleapis/sdk-platform-java";
CodeGeneratorRequest request =
CodeGeneratorRequest.newBuilder().setParameter(createRepo(repo)).build();
assertEquals(repo, PluginArgumentParser.parseRepo(request).get());
}

@Test
void parseRepo_noneFound() {
CodeGeneratorRequest request =
CodeGeneratorRequest.newBuilder().setParameter("metadata").build();
assertFalse(PluginArgumentParser.parseRepo(request).isPresent());
}

private static String createGrpcServiceConfig(String path) {
return String.format("%s=%s", PluginArgumentParser.KEY_GRPC_SERVICE_CONFIG, path);
}
Expand All @@ -280,4 +296,8 @@ private static String createGapicConfig(String path) {
private static String createServiceConfig(String path) {
return String.format("%s=%s", PluginArgumentParser.KEY_SERVICE_YAML_CONFIG, path);
}

private static String createRepo(String repo) {
return String.format("%s=%s", KEY_REPO, repo);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import com.google.gson.Gson;
import com.google.protobuf.ByteString;
import com.google.protobuf.compiler.PluginProtos.CodeGeneratorResponse;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
Expand All @@ -25,6 +26,7 @@
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Optional;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
Expand Down Expand Up @@ -144,4 +146,26 @@ void productionWrite_emptyGapicContext_succeeds() throws IOException {
"temp-codegen.srcjar");
assertNull(result);
}

@Test
void writeRepoFile_isWritten() throws IOException {
String repo = "googleapis/sdk-platform-java";
GapicContext context = GapicContext.EMPTY.toBuilder().setRepo(Optional.of(repo)).build();
Writer.writeGapicPropertiesFile(context, jarOutputStream);

closeJarOutputStream();

try (JarFile jarFile = new JarFile(file)) {
Enumeration<JarEntry> entries = jarFile.entries();
assertThat(entries.hasMoreElements()).isTrue();
JarEntry entry = entries.nextElement();
assertThat(entries.hasMoreElements()).isFalse();
assertEquals("src/main/resources/gapic.properties", entry.getName());
try (BufferedReader reader =
new BufferedReader(new InputStreamReader(jarFile.getInputStream(entry)))) {
String line = reader.readLine();
assertEquals("repo=" + repo, line);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@

import com.google.api.core.InternalApi;
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;

/**
Expand All @@ -43,18 +49,52 @@
@InternalApi
@AutoValue
public abstract class ApiTracerContext {
private static final Logger LOGGER = Logger.getLogger(ApiTracerContext.class.getName());
private static final String GAPIC_PROPERTIES_FILE = "/gapic.properties";
private static final String REPO_KEY = "repo";

@Nullable
public abstract String getServerAddress();

@Nullable
public abstract String getRepo();

public static Builder newBuilder() {
return new AutoValue_ApiTracerContext.Builder();
return newBuilder(ApiTracerContext.class.getResourceAsStream(GAPIC_PROPERTIES_FILE));
}

@VisibleForTesting
static Builder newBuilder(@Nullable InputStream inputStream) {
Builder builder = new AutoValue_ApiTracerContext.Builder();
loadRepoFromProperties(builder, inputStream);
return builder;
}

private static void loadRepoFromProperties(Builder builder, @Nullable InputStream is) {
if (is == null) {
return;
}
try {
Properties properties = new Properties();
properties.load(is);
builder.setRepo(properties.getProperty(REPO_KEY));
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Could not load gapic.properties", e);
} finally {
try {
is.close();
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Could not close gapic.properties stream", e);
}
}
}
Comment on lines +73 to 90
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The loadRepoFromProperties method can be simplified by using a try-with-resources statement. This is the modern and recommended way to handle resources like InputStream in Java, as it ensures the stream is closed correctly and makes the code more concise and readable.

  private static void loadRepoFromProperties(Builder builder, @Nullable InputStream is) {
    if (is == null) {
      return;
    }
    try (InputStream inputStream = is) {
      Properties properties = new Properties();
      properties.load(inputStream);
      builder.setRepo(properties.getProperty(REPO_KEY));
    } catch (IOException e) {
      LOGGER.log(Level.WARNING, "Could not load or close gapic.properties", e);
    }
  }


@AutoValue.Builder
public abstract static class Builder {
public abstract Builder setServerAddress(String serverAddress);

public abstract Builder setRepo(String repo);

public abstract ApiTracerContext build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
@InternalApi
public class AppCentricTracer implements ApiTracer {
public static final String LANGUAGE_ATTRIBUTE = "gcp.client.language";
public static final String REPO_ATTRIBUTE = "gcp.client.repo";

public static final String DEFAULT_LANGUAGE = "Java";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ public ApiTracer newTracer(ApiTracer parent, SpanName spanName, OperationType op
public ApiTracerFactory withContext(ApiTracerContext context) {
Map<String, String> newAttemptAttributes = new HashMap<>(this.attemptAttributes);
newAttemptAttributes.putAll(AppCentricAttributes.getAttemptAttributes(context));
if (context.getRepo() != null) {
newAttemptAttributes.put(AppCentricTracer.REPO_ATTRIBUTE, context.getRepo());
}
return new AppCentricTracerFactory(traceRecorder, operationAttributes, newAttemptAttributes);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright 2026 Google LLC
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google LLC nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

package com.google.api.gax.tracing;

import static com.google.common.truth.Truth.assertThat;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

class ApiTracerContextTest {

@Test
void testNewBuilderWithNoPropertiesFile() {
ApiTracerContext context =
ApiTracerContext.newBuilder(null).setServerAddress("test-address").build();

assertThat(context.getServerAddress()).isEqualTo("test-address");
assertThat(context.getRepo()).isNull();
}

@Test
void testNewBuilderWithPropertiesFile() {
String propertiesContent = "repo=test-repo";
InputStream inputStream =
new ByteArrayInputStream(propertiesContent.getBytes(StandardCharsets.UTF_8));

ApiTracerContext context =
ApiTracerContext.newBuilder(inputStream).setServerAddress("test-address").build();

assertThat(context.getServerAddress()).isEqualTo("test-address");
assertThat(context.getRepo()).isEqualTo("test-repo");
}

@Test
void testNewBuilderWithPropertiesFileAndNoRepoKey() {
String propertiesContent = "somekey=somevalue";
InputStream inputStream =
new ByteArrayInputStream(propertiesContent.getBytes(StandardCharsets.UTF_8));

ApiTracerContext context =
ApiTracerContext.newBuilder(inputStream).setServerAddress("test-address").build();

assertThat(context.getServerAddress()).isEqualTo("test-address");
assertThat(context.getRepo()).isNull();
}

@Test
void testNewBuilderWithPropertiesFileLoadingError() throws IOException {
InputStream mockInputStream = Mockito.mock(InputStream.class);
Mockito.doThrow(new IOException("Test IO Exception"))
.when(mockInputStream)
.read(Mockito.any(byte[].class));

ApiTracerContext context =
ApiTracerContext.newBuilder(mockInputStream).setServerAddress("test-address").build();

assertThat(context.getServerAddress()).isEqualTo("test-address");
assertThat(context.getRepo()).isNull();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ void testWithContext_addsInferredAttributes() {
when(recorder.createSpan(anyString(), anyMap())).thenReturn(attemptHandle);

ApiTracerContext context =
ApiTracerContext.newBuilder().setServerAddress("example.com").build();
ApiTracerContext.newBuilder().setServerAddress("example.com").setRepo("my-repo").build();

AppCentricTracerFactory factory = new AppCentricTracerFactory(recorder);
ApiTracerFactory factoryWithContext = factory.withContext(context);
Expand All @@ -104,6 +104,7 @@ void testWithContext_addsInferredAttributes() {
Map<String, String> attemptAttributes = attributesCaptor.getValue();
assertThat(attemptAttributes)
.containsEntry(AppCentricAttributes.SERVER_ADDRESS_ATTRIBUTE, "example.com");
assertThat(attemptAttributes).containsEntry(AppCentricTracer.REPO_ATTRIBUTE, "my-repo");
}

@Test
Expand All @@ -112,7 +113,7 @@ void testWithContext_noEndpointContext_doesNotAddAttributes() {
TraceRecorder.TraceSpan attemptHandle = mock(TraceRecorder.TraceSpan.class);
when(recorder.createSpan(anyString(), anyMap())).thenReturn(attemptHandle);

ApiTracerContext context = ApiTracerContext.newBuilder().build();
ApiTracerContext context = ApiTracerContext.newBuilder(null).build();

AppCentricTracerFactory factory = new AppCentricTracerFactory(recorder);
ApiTracerFactory factoryWithContext = factory.withContext(context);
Expand All @@ -128,5 +129,6 @@ void testWithContext_noEndpointContext_doesNotAddAttributes() {

Map<String, String> attemptAttributes = attributesCaptor.getValue();
assertThat(attemptAttributes).doesNotContainKey(AppCentricAttributes.SERVER_ADDRESS_ATTRIBUTE);
assertThat(attemptAttributes).doesNotContainKey(AppCentricTracer.REPO_ATTRIBUTE);
}
}
Loading
Loading