From 37f181cb6ed0237f43d81159eb81b19d3b5f8e36 Mon Sep 17 00:00:00 2001 From: Yannic Bonenberger Date: Fri, 8 Jul 2022 01:40:43 -0700 Subject: [PATCH] [credentialhelper] Add types to communicate with the subprocess This change adds `GetCredentials{Request,Response}`, which we'll use to pass data in `stdin` to the credental helper subprocess, and read the response from `stdout` of the subprocess. Progress on https://github.com/bazelbuild/proposals/blob/main/designs/2022-06-07-bazel-credential-helpers.md#proposal Closes #15803. PiperOrigin-RevId: 459701706 Change-Id: Icbd9fc491546ee5599d5a9d04680671b06a91a85 --- .../lib/authandtls/credentialhelper/BUILD | 2 + .../GetCredentialsRequest.java | 103 ++++++++++++ .../GetCredentialsResponse.java | 154 +++++++++++++++++ .../lib/authandtls/credentialhelper/BUILD | 1 + .../GetCredentialsRequestTest.java | 116 +++++++++++++ .../GetCredentialsResponseTest.java | 158 ++++++++++++++++++ 6 files changed, 534 insertions(+) create mode 100644 src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/GetCredentialsRequest.java create mode 100644 src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/GetCredentialsResponse.java create mode 100644 src/test/java/com/google/devtools/build/lib/authandtls/credentialhelper/GetCredentialsRequestTest.java create mode 100644 src/test/java/com/google/devtools/build/lib/authandtls/credentialhelper/GetCredentialsResponseTest.java diff --git a/src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/BUILD b/src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/BUILD index caed6e13c058d3..745e6eaebae2b7 100644 --- a/src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/BUILD +++ b/src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/BUILD @@ -15,7 +15,9 @@ java_library( srcs = glob(["*.java"]), deps = [ "//src/main/java/com/google/devtools/build/lib/vfs", + "//third_party:auto_value", "//third_party:error_prone_annotations", + "//third_party:gson", "//third_party:guava", ], ) diff --git a/src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/GetCredentialsRequest.java b/src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/GetCredentialsRequest.java new file mode 100644 index 00000000000000..3915a861600732 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/GetCredentialsRequest.java @@ -0,0 +1,103 @@ +// Copyright 2022 The Bazel Authors. All rights reserved. +// +// 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 com.google.devtools.build.lib.authandtls.credentialhelper; + +import com.google.auto.value.AutoValue; +import com.google.common.base.Preconditions; +import com.google.errorprone.annotations.Immutable; +import com.google.gson.JsonSyntaxException; +import com.google.gson.TypeAdapter; +import com.google.gson.annotations.JsonAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.net.URI; +import java.util.Locale; + +/** + * Request for the {@code get} command of the Credential + * Helper Protocol. + */ +@AutoValue +@AutoValue.CopyAnnotations +@Immutable +@JsonAdapter(GetCredentialsRequest.GsonTypeAdapter.class) +public abstract class GetCredentialsRequest { + /** Returns the {@link URI} this request is for. */ + public abstract URI getUri(); + + /** Returns a new builder for {@link GetCredentialsRequest}. */ + public static Builder newBuilder() { + return new AutoValue_GetCredentialsRequest.Builder(); + } + + /** Builder for {@link GetCredentialsRequest}. */ + @AutoValue.Builder + public abstract static class Builder { + /** Sets the {@link URI} this request is for. */ + public abstract Builder setUri(URI uri); + + /** Returns the newly constructed {@link GetCredentialsRequest}. */ + public abstract GetCredentialsRequest build(); + } + + /** GSON adapter for GetCredentialsRequest. */ + public static final class GsonTypeAdapter extends TypeAdapter { + @Override + public void write(JsonWriter writer, GetCredentialsRequest value) throws IOException { + Preconditions.checkNotNull(writer); + Preconditions.checkNotNull(value); + + writer.beginObject(); + writer.name("uri").value(value.getUri().toString()); + writer.endObject(); + } + + @Override + public GetCredentialsRequest read(JsonReader reader) throws IOException { + Preconditions.checkNotNull(reader); + + Builder request = newBuilder(); + + if (reader.peek() != JsonToken.BEGIN_OBJECT) { + throw new JsonSyntaxException( + String.format(Locale.US, "Expected object, got %s", reader.peek())); + } + reader.beginObject(); + while (reader.hasNext()) { + String name = reader.nextName(); + switch (name) { + case "uri": + if (reader.peek() != JsonToken.STRING) { + throw new JsonSyntaxException( + String.format( + Locale.US, "Expected value of 'url' to be a string, got %s", reader.peek())); + } + request.setUri(URI.create(reader.nextString())); + break; + + default: + // We intentionally ignore unknown keys to achieve forward compatibility with requests + // coming from newer tools. + reader.skipValue(); + } + } + reader.endObject(); + return request.build(); + } + } +} diff --git a/src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/GetCredentialsResponse.java b/src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/GetCredentialsResponse.java new file mode 100644 index 00000000000000..72f25cf4ddf196 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/GetCredentialsResponse.java @@ -0,0 +1,154 @@ +// Copyright 2022 The Bazel Authors. All rights reserved. +// +// 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 com.google.devtools.build.lib.authandtls.credentialhelper; + +import com.google.auto.value.AutoValue; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.errorprone.annotations.Immutable; +import com.google.gson.JsonSyntaxException; +import com.google.gson.TypeAdapter; +import com.google.gson.annotations.JsonAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.util.Locale; +import java.util.Map; + +/** + * Response from the {@code get} command of the Credential + * Helper Protocol. + */ +@AutoValue +@AutoValue.CopyAnnotations +@Immutable +@JsonAdapter(GetCredentialsResponse.GsonTypeAdapter.class) +public abstract class GetCredentialsResponse { + /** Returns the headers to attach to the request. */ + public abstract ImmutableMap> getHeaders(); + + /** Returns a new builder for {@link GetCredentialsRequest}. */ + public static Builder newBuilder() { + return new AutoValue_GetCredentialsResponse.Builder(); + } + + /** Builder for {@link GetCredentialsResponse}. */ + @AutoValue.Builder + public abstract static class Builder { + protected abstract ImmutableMap.Builder> headersBuilder(); + + /** Returns the newly constructed {@link GetCredentialsResponse}. */ + public abstract GetCredentialsResponse build(); + } + + /** GSON adapter for GetCredentialsResponse. */ + public static final class GsonTypeAdapter extends TypeAdapter { + @Override + public void write(JsonWriter writer, GetCredentialsResponse response) throws IOException { + Preconditions.checkNotNull(writer); + Preconditions.checkNotNull(response); + + writer.beginObject(); + + ImmutableMap> headers = response.getHeaders(); + if (!headers.isEmpty()) { + writer.name("headers"); + writer.beginObject(); + for (Map.Entry> entry : headers.entrySet()) { + writer.name(entry.getKey()); + + writer.beginArray(); + for (String value : entry.getValue()) { + writer.value(value); + } + writer.endArray(); + } + writer.endObject(); + } + writer.endObject(); + } + + @Override + public GetCredentialsResponse read(JsonReader reader) throws IOException { + Preconditions.checkNotNull(reader); + + GetCredentialsResponse.Builder response = newBuilder(); + + if (reader.peek() != JsonToken.BEGIN_OBJECT) { + throw new JsonSyntaxException( + String.format(Locale.US, "Expected object, got %s", reader.peek())); + } + reader.beginObject(); + + while (reader.hasNext()) { + String name = reader.nextName(); + switch (name) { + case "headers": + if (reader.peek() != JsonToken.BEGIN_OBJECT) { + throw new JsonSyntaxException( + String.format( + Locale.US, + "Expected value of 'headers' to be an object, got %s", + reader.peek())); + } + reader.beginObject(); + + while (reader.hasNext()) { + String headerName = reader.nextName(); + ImmutableList.Builder headerValues = ImmutableList.builder(); + + if (reader.peek() != JsonToken.BEGIN_ARRAY) { + throw new JsonSyntaxException( + String.format( + Locale.US, + "Expected value of '%s' header to be an array of strings, got %s", + headerName, + reader.peek())); + } + reader.beginArray(); + for (int i = 0; reader.hasNext(); i++) { + if (reader.peek() != JsonToken.STRING) { + throw new JsonSyntaxException( + String.format( + Locale.US, + "Expected value %s of '%s' header to be a string, got %s", + i, + headerName, + reader.peek())); + } + headerValues.add(reader.nextString()); + } + reader.endArray(); + + response.headersBuilder().put(headerName, headerValues.build()); + } + + reader.endObject(); + break; + + default: + // We intentionally ignore unknown keys to achieve forward compatibility with responses + // coming from newer tools. + reader.skipValue(); + } + } + reader.endObject(); + return response.build(); + } + } +} diff --git a/src/test/java/com/google/devtools/build/lib/authandtls/credentialhelper/BUILD b/src/test/java/com/google/devtools/build/lib/authandtls/credentialhelper/BUILD index e904e6d99349fb..95b3ee4483cdc9 100644 --- a/src/test/java/com/google/devtools/build/lib/authandtls/credentialhelper/BUILD +++ b/src/test/java/com/google/devtools/build/lib/authandtls/credentialhelper/BUILD @@ -26,6 +26,7 @@ java_test( "//src/main/java/com/google/devtools/build/lib/vfs", "//src/main/java/com/google/devtools/build/lib/vfs:pathfragment", "//src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs", + "//third_party:gson", "//third_party:guava", "//third_party:junit4", "//third_party:truth", diff --git a/src/test/java/com/google/devtools/build/lib/authandtls/credentialhelper/GetCredentialsRequestTest.java b/src/test/java/com/google/devtools/build/lib/authandtls/credentialhelper/GetCredentialsRequestTest.java new file mode 100644 index 00000000000000..0e0939ef551ca1 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/authandtls/credentialhelper/GetCredentialsRequestTest.java @@ -0,0 +1,116 @@ +// Copyright 2022 The Bazel Authors. All rights reserved. +// +// 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 com.google.devtools.build.lib.authandtls.credentialhelper; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; +import java.net.URI; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link GetCredentialsRequest}. */ +@RunWith(JUnit4.class) +public class GetCredentialsRequestTest { + private static final Gson GSON = new Gson(); + + @Test + public void parseValid() { + assertThat( + GSON.fromJson("{\"uri\": \"http://example.com\"}", GetCredentialsRequest.class) + .getUri()) + .isEqualTo(URI.create("http://example.com")); + assertThat( + GSON.fromJson("{\"uri\": \"https://example.com\"}", GetCredentialsRequest.class) + .getUri()) + .isEqualTo(URI.create("https://example.com")); + assertThat( + GSON.fromJson("{\"uri\": \"grpc://example.com\"}", GetCredentialsRequest.class) + .getUri()) + .isEqualTo(URI.create("grpc://example.com")); + assertThat( + GSON.fromJson("{\"uri\": \"grpcs://example.com\"}", GetCredentialsRequest.class) + .getUri()) + .isEqualTo(URI.create("grpcs://example.com")); + + assertThat( + GSON.fromJson("{\"uri\": \"uri-without-protocol\"}", GetCredentialsRequest.class) + .getUri()) + .isEqualTo(URI.create("uri-without-protocol")); + } + + @Test + public void parseMissingUri() { + assertThrows(JsonSyntaxException.class, () -> GSON.fromJson("{}", GetCredentialsRequest.class)); + assertThrows( + JsonSyntaxException.class, + () -> GSON.fromJson("{\"foo\": 1}", GetCredentialsRequest.class)); + assertThrows( + JsonSyntaxException.class, + () -> GSON.fromJson("{\"foo\": 1, \"bar\": 2}", GetCredentialsRequest.class)); + } + + @Test + public void parseNonStringUri() { + assertThrows(JsonSyntaxException.class, () -> GSON.fromJson("[]", GetCredentialsRequest.class)); + assertThrows( + JsonSyntaxException.class, () -> GSON.fromJson("\"foo\"", GetCredentialsRequest.class)); + assertThrows(JsonSyntaxException.class, () -> GSON.fromJson("1", GetCredentialsRequest.class)); + assertThrows( + JsonSyntaxException.class, + () -> GSON.fromJson("{\"uri\": 1}", GetCredentialsRequest.class)); + assertThrows( + JsonSyntaxException.class, + () -> GSON.fromJson("{\"uri\": {}}", GetCredentialsRequest.class)); + assertThrows( + JsonSyntaxException.class, + () -> GSON.fromJson("{\"uri\": []}", GetCredentialsRequest.class)); + assertThrows( + JsonSyntaxException.class, + () -> GSON.fromJson("{\"uri\": [\"https://example.com\"]}", GetCredentialsRequest.class)); + assertThrows( + JsonSyntaxException.class, + () -> GSON.fromJson("{\"uri\": null}", GetCredentialsRequest.class)); + } + + @Test + public void parseWithExtraFields() { + assertThat( + GSON.fromJson( + "{\"uri\": \"http://example.com\", \"foo\": 1}", GetCredentialsRequest.class) + .getUri()) + .isEqualTo(URI.create("http://example.com")); + assertThat( + GSON.fromJson( + "{\"foo\": 1, \"uri\": \"http://example.com\"}", GetCredentialsRequest.class) + .getUri()) + .isEqualTo(URI.create("http://example.com")); + assertThat( + GSON.fromJson( + "{\"uri\": \"http://example.com\", \"foo\": 1, \"bar\": {}}", + GetCredentialsRequest.class) + .getUri()) + .isEqualTo(URI.create("http://example.com")); + assertThat( + GSON.fromJson( + "{\"foo\": 1, \"uri\": \"http://example.com\", \"bar\": []}", + GetCredentialsRequest.class) + .getUri()) + .isEqualTo(URI.create("http://example.com")); + } +} diff --git a/src/test/java/com/google/devtools/build/lib/authandtls/credentialhelper/GetCredentialsResponseTest.java b/src/test/java/com/google/devtools/build/lib/authandtls/credentialhelper/GetCredentialsResponseTest.java new file mode 100644 index 00000000000000..8d1d8f56c34ef2 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/authandtls/credentialhelper/GetCredentialsResponseTest.java @@ -0,0 +1,158 @@ +// Copyright 2022 The Bazel Authors. All rights reserved. +// +// 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 com.google.devtools.build.lib.authandtls.credentialhelper; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableList; +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link GetCredentialsResponse}. */ +@RunWith(JUnit4.class) +public class GetCredentialsResponseTest { + private static final Gson GSON = new Gson(); + + @Test + public void parseValid() { + assertThat(GSON.fromJson("{}", GetCredentialsResponse.class).getHeaders()).isEmpty(); + assertThat(GSON.fromJson("{\"headers\": {}}", GetCredentialsResponse.class).getHeaders()) + .isEmpty(); + + GetCredentialsResponse.Builder expectedResponseBuilder = GetCredentialsResponse.newBuilder(); + expectedResponseBuilder.headersBuilder().put("a", ImmutableList.of()); + expectedResponseBuilder.headersBuilder().put("b", ImmutableList.of("b")); + expectedResponseBuilder.headersBuilder().put("c", ImmutableList.of("c", "c")); + GetCredentialsResponse expectedResponse = expectedResponseBuilder.build(); + + assertThat( + GSON.fromJson( + "{\"headers\": {\"c\": [\"c\", \"c\"], \"a\": [], \"b\": [\"b\"]}}", + GetCredentialsResponse.class)) + .isEqualTo(expectedResponse); + } + + @Test + public void parseWithExtraFields() { + assertThat(GSON.fromJson("{\"foo\": 123}", GetCredentialsResponse.class).getHeaders()) + .isEmpty(); + assertThat( + GSON.fromJson("{\"foo\": 123, \"bar\": []}", GetCredentialsResponse.class).getHeaders()) + .isEmpty(); + + GetCredentialsResponse.Builder expectedResponseBuilder = GetCredentialsResponse.newBuilder(); + expectedResponseBuilder.headersBuilder().put("a", ImmutableList.of()); + expectedResponseBuilder.headersBuilder().put("b", ImmutableList.of("b")); + expectedResponseBuilder.headersBuilder().put("c", ImmutableList.of("c", "c")); + GetCredentialsResponse expectedResponse = expectedResponseBuilder.build(); + + assertThat( + GSON.fromJson( + "{\"foo\": 123, \"headers\": {\"c\": [\"c\", \"c\"], \"a\": [], \"b\": [\"b\"]}," + + " \"bar\": 123}", + GetCredentialsResponse.class)) + .isEqualTo(expectedResponse); + } + + @Test + public void parseInvalid() { + assertThrows( + JsonSyntaxException.class, () -> GSON.fromJson("[]", GetCredentialsResponse.class)); + assertThrows( + JsonSyntaxException.class, () -> GSON.fromJson("\"foo\"", GetCredentialsResponse.class)); + assertThrows(JsonSyntaxException.class, () -> GSON.fromJson("1", GetCredentialsResponse.class)); + } + + @Test + public void parseInvalidHeadersEnvelope() { + assertThrows( + JsonSyntaxException.class, + () -> GSON.fromJson("{\"headers\": null}", GetCredentialsResponse.class)); + assertThrows( + JsonSyntaxException.class, + () -> GSON.fromJson("{\"headers\": \"foo\"}", GetCredentialsResponse.class)); + assertThrows( + JsonSyntaxException.class, + () -> GSON.fromJson("{\"headers\": []}", GetCredentialsResponse.class)); + assertThrows( + JsonSyntaxException.class, + () -> GSON.fromJson("{\"headers\": 1}", GetCredentialsResponse.class)); + } + + @Test + public void parseInvalidHeadersValue() { + assertThrows( + JsonSyntaxException.class, + () -> GSON.fromJson("{\"headers\": {\"a\": null}}", GetCredentialsResponse.class)); + assertThrows( + JsonSyntaxException.class, + () -> GSON.fromJson("{\"headers\": {\"a\": 1}}", GetCredentialsResponse.class)); + assertThrows( + JsonSyntaxException.class, + () -> GSON.fromJson("{\"headers\": {\"a\": {}}}", GetCredentialsResponse.class)); + assertThrows( + JsonSyntaxException.class, + () -> GSON.fromJson("{\"headers\": {\"a\": \"a\"}}", GetCredentialsResponse.class)); + assertThrows( + JsonSyntaxException.class, + () -> GSON.fromJson("{\"headers\": {\"a\": [null]}}", GetCredentialsResponse.class)); + assertThrows( + JsonSyntaxException.class, + () -> GSON.fromJson("{\"headers\": {\"a\": [\"a\", null]}}", GetCredentialsResponse.class)); + assertThrows( + JsonSyntaxException.class, + () -> GSON.fromJson("{\"headers\": {\"a\": [null, \"a\"]}}", GetCredentialsResponse.class)); + assertThrows( + JsonSyntaxException.class, + () -> GSON.fromJson("{\"headers\": {\"a\": [\"a\", 1]}}", GetCredentialsResponse.class)); + assertThrows( + JsonSyntaxException.class, + () -> GSON.fromJson("{\"headers\": {\"a\": [1, \"a\"]}}", GetCredentialsResponse.class)); + assertThrows( + JsonSyntaxException.class, + () -> GSON.fromJson("{\"headers\": {\"a\": [\"a\", []]}}", GetCredentialsResponse.class)); + assertThrows( + JsonSyntaxException.class, + () -> GSON.fromJson("{\"headers\": {\"a\": [[], \"a\"]}}", GetCredentialsResponse.class)); + assertThrows( + JsonSyntaxException.class, + () -> GSON.fromJson("{\"headers\": {\"a\": [\"a\", {}]}}", GetCredentialsResponse.class)); + assertThrows( + JsonSyntaxException.class, + () -> GSON.fromJson("{\"headers\": {\"a\": [{}, \"a\"]}}", GetCredentialsResponse.class)); + } + + @Test + public void serializeEmptyHeaders() { + GetCredentialsResponse expectedResponse = GetCredentialsResponse.newBuilder().build(); + assertThat(GSON.toJson(expectedResponse)).isEqualTo("{}"); + } + + @Test + public void roundTrip() { + GetCredentialsResponse.Builder expectedResponseBuilder = GetCredentialsResponse.newBuilder(); + expectedResponseBuilder.headersBuilder().put("a", ImmutableList.of()); + expectedResponseBuilder.headersBuilder().put("b", ImmutableList.of("b")); + expectedResponseBuilder.headersBuilder().put("c", ImmutableList.of("c", "c")); + GetCredentialsResponse expectedResponse = expectedResponseBuilder.build(); + + assertThat(GSON.fromJson(GSON.toJson(expectedResponse), GetCredentialsResponse.class)) + .isEqualTo(expectedResponse); + } +}