From 4175018b47800db28c390d39fefbd266b5d674bd Mon Sep 17 00:00:00 2001 From: Yannic Bonenberger Date: Fri, 1 Jul 2022 06:32:12 -0700 Subject: [PATCH] Add util for finding credential helper to use Progress on https://github.com/bazelbuild/proposals/blob/main/designs/2022-06-07-bazel-credential-helpers.md Closes #15707. PiperOrigin-RevId: 458456496 Change-Id: I751a594144c3563096ee9794c41329b49755824e --- .../devtools/build/lib/authandtls/BUILD | 4 +- .../lib/authandtls/credentialhelper/BUILD | 21 + .../credentialhelper/CredentialHelper.java | 39 ++ .../CredentialHelperProvider.java | 190 +++++++++ .../devtools/build/lib/authandtls/BUILD | 4 +- .../lib/authandtls/credentialhelper/BUILD | 33 ++ .../CredentialHelperProviderTest.java | 376 ++++++++++++++++++ 7 files changed, 665 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/BUILD create mode 100644 src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/CredentialHelper.java create mode 100644 src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/CredentialHelperProvider.java create mode 100644 src/test/java/com/google/devtools/build/lib/authandtls/credentialhelper/BUILD create mode 100644 src/test/java/com/google/devtools/build/lib/authandtls/credentialhelper/CredentialHelperProviderTest.java diff --git a/src/main/java/com/google/devtools/build/lib/authandtls/BUILD b/src/main/java/com/google/devtools/build/lib/authandtls/BUILD index 1908736e03c8f3..23d2909923954d 100644 --- a/src/main/java/com/google/devtools/build/lib/authandtls/BUILD +++ b/src/main/java/com/google/devtools/build/lib/authandtls/BUILD @@ -4,7 +4,9 @@ package(default_visibility = ["//src:__subpackages__"]) filegroup( name = "srcs", - srcs = glob(["**"]), + srcs = glob(["**"]) + [ + "//src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper:srcs", + ], visibility = ["//src:__subpackages__"], ) 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 new file mode 100644 index 00000000000000..caed6e13c058d3 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/BUILD @@ -0,0 +1,21 @@ +load("@rules_java//java:defs.bzl", "java_library") + +package(default_visibility = ["//src:__subpackages__"]) + +licenses(["notice"]) + +filegroup( + name = "srcs", + srcs = glob(["**"]), + visibility = ["//src:__subpackages__"], +) + +java_library( + name = "credentialhelper", + srcs = glob(["*.java"]), + deps = [ + "//src/main/java/com/google/devtools/build/lib/vfs", + "//third_party:error_prone_annotations", + "//third_party:guava", + ], +) diff --git a/src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/CredentialHelper.java b/src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/CredentialHelper.java new file mode 100644 index 00000000000000..c6b60b1fb1a0b4 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/CredentialHelper.java @@ -0,0 +1,39 @@ +// 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.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.devtools.build.lib.vfs.Path; +import com.google.errorprone.annotations.Immutable; + +/** Wraps an external tool used to obtain credentials. */ +@Immutable +public final class CredentialHelper { + // `Path` is immutable, but not annotated. + @SuppressWarnings("Immutable") + private final Path path; + + CredentialHelper(Path path) { + this.path = Preconditions.checkNotNull(path); + } + + @VisibleForTesting + Path getPath() { + return path; + } + + // TODO(yannic): Implement running the helper subprocess. +} diff --git a/src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/CredentialHelperProvider.java b/src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/CredentialHelperProvider.java new file mode 100644 index 00000000000000..ded7e5eab3c0d0 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper/CredentialHelperProvider.java @@ -0,0 +1,190 @@ +// 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.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import com.google.devtools.build.lib.vfs.Path; +import com.google.errorprone.annotations.Immutable; +import java.io.IOException; +import java.net.IDN; +import java.net.URI; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Pattern; + +/** + * A provider for {@link CredentialHelper}s. + * + *

This class is used to find the right {@link CredentialHelper} for a {@link URI}, using the + * most specific match. + */ +@Immutable +public final class CredentialHelperProvider { + // `Path` is immutable, but not annotated. + @SuppressWarnings("Immutable") + private final Optional defaultHelper; + + @SuppressWarnings("Immutable") + private final ImmutableMap hostToHelper; + + @SuppressWarnings("Immutable") + private final ImmutableMap suffixToHelper; + + private CredentialHelperProvider( + Optional defaultHelper, + ImmutableMap hostToHelper, + ImmutableMap suffixToHelper) { + this.defaultHelper = Preconditions.checkNotNull(defaultHelper); + this.hostToHelper = Preconditions.checkNotNull(hostToHelper); + this.suffixToHelper = Preconditions.checkNotNull(suffixToHelper); + } + + /** + * Returns {@link CredentialHelper} to use for getting credentials for connection to the provided + * {@link URI}. + * + * @param uri The {@link URI} to get a credential helper for. + * @return The {@link CredentialHelper}, or nothing if no {@link CredentialHelper} is configured + * for the provided {@link URI}. + */ + public Optional findCredentialHelper(URI uri) { + Preconditions.checkNotNull(uri); + + String host = Preconditions.checkNotNull(uri.getHost()); + Optional credentialHelper = + findHostCredentialHelper(host) + .or(() -> findWildcardCredentialHelper(host)) + .or(() -> defaultHelper); + return credentialHelper.map(CredentialHelper::new); + } + + private Optional findHostCredentialHelper(String host) { + Preconditions.checkNotNull(host); + + return Optional.ofNullable(hostToHelper.get(host)); + } + + private Optional findWildcardCredentialHelper(String host) { + Preconditions.checkNotNull(host); + + return Optional.ofNullable(suffixToHelper.get(host)) + .or( + () -> { + Optional subdomain = parentDomain(host); + if (subdomain.isEmpty()) { + return Optional.empty(); + } + return findWildcardCredentialHelper(subdomain.get()); + }); + } + + /** + * Returns the parent domain of the provided domain (e.g., {@code foo.example.com} for {@code + * bar.foo.example.com}). + */ + @VisibleForTesting + static Optional parentDomain(String domain) { + int dot = domain.indexOf('.'); + if (dot < 0) { + // We reached the last segment, end. + return Optional.empty(); + } + + return Optional.of(domain.substring(dot + 1)); + } + + /** Returns a new builder for a {@link CredentialHelperProvider}. */ + public static Builder builder() { + return new Builder(); + } + + /** Builder for {@link CredentialHelperProvider}. */ + public static final class Builder { + private static final Pattern DOMAIN_PATTERN = + Pattern.compile("(\\*|[-a-zA-Z0-9]+)(\\.[-a-zA-Z0-9]+)+"); + + private Optional defaultHelper = Optional.empty(); + private final Map hostToHelper = new HashMap<>(); + private final Map suffixToHelper = new HashMap<>(); + + private void checkHelper(Path path) throws IOException { + Preconditions.checkNotNull(path); + Preconditions.checkArgument( + path.isExecutable(), "Credential helper %s is not executable", path); + } + + /** + * Adds a default credential helper to use for all {@link URI}s that don't specify a more + * specific credential helper. + */ + public Builder add(Path helper) throws IOException { + checkHelper(helper); + + defaultHelper = Optional.of(helper); + return this; + } + + /** + * Adds a credential helper to use for all {@link URI}s matching the provided pattern. + * + *

As of 2022-06-20, only matching based on (wildcard) domain name is supported. + * + *

If {@code pattern} starts with {@code *.}, it is considered a wildcard pattern matching + * all subdomains in addition to the domain itself. For example {@code *.example.com} would + * match {@code example.com}, {@code foo.example.com}, {@code bar.example.com}, {@code + * baz.bar.example.com} and so on, but not anything that isn't a subdomain of {@code + * example.com}. + */ + public Builder add(String pattern, Path helper) throws IOException { + Preconditions.checkNotNull(pattern); + checkHelper(helper); + + String punycodePattern = toPunycodePattern(pattern); + Preconditions.checkArgument( + DOMAIN_PATTERN.matcher(punycodePattern).matches(), + "Pattern '%s' is not a valid (wildcard) DNS name", + pattern); + + if (pattern.startsWith("*.")) { + suffixToHelper.put(punycodePattern.substring(2), helper); + } else { + hostToHelper.put(punycodePattern, helper); + } + + return this; + } + + /** Converts a pattern to Punycode (see https://en.wikipedia.org/wiki/Punycode). */ + private final String toPunycodePattern(String pattern) { + Preconditions.checkNotNull(pattern); + + try { + return IDN.toASCII(pattern); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException( + String.format(Locale.US, "Could not convert '%s' to punycode", pattern), e); + } + } + + public CredentialHelperProvider build() { + return new CredentialHelperProvider( + defaultHelper, ImmutableMap.copyOf(hostToHelper), ImmutableMap.copyOf(suffixToHelper)); + } + } +} diff --git a/src/test/java/com/google/devtools/build/lib/authandtls/BUILD b/src/test/java/com/google/devtools/build/lib/authandtls/BUILD index 7bef4b2873c15e..f9125da2d92771 100644 --- a/src/test/java/com/google/devtools/build/lib/authandtls/BUILD +++ b/src/test/java/com/google/devtools/build/lib/authandtls/BUILD @@ -10,7 +10,9 @@ licenses(["notice"]) filegroup( name = "srcs", testonly = 0, - srcs = glob(["**"]), + srcs = glob(["**"]) + [ + "//src/test/java/com/google/devtools/build/lib/authandtls/credentialhelper:srcs", + ], visibility = ["//src:__subpackages__"], ) 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 new file mode 100644 index 00000000000000..e904e6d99349fb --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/authandtls/credentialhelper/BUILD @@ -0,0 +1,33 @@ +load("@rules_java//java:defs.bzl", "java_test") + +package( + default_testonly = 1, + default_visibility = ["//src:__subpackages__"], +) + +licenses(["notice"]) + +filegroup( + name = "srcs", + testonly = 0, + srcs = glob(["**"]), + visibility = ["//src:__subpackages__"], +) + +java_test( + name = "credentialhelper", + srcs = glob(["*.java"]), + test_class = "com.google.devtools.build.lib.AllTests", + runtime_deps = [ + "//src/test/java/com/google/devtools/build/lib:test_runner", + ], + deps = [ + "//src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper", + "//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:guava", + "//third_party:junit4", + "//third_party:truth", + ], +) diff --git a/src/test/java/com/google/devtools/build/lib/authandtls/credentialhelper/CredentialHelperProviderTest.java b/src/test/java/com/google/devtools/build/lib/authandtls/credentialhelper/CredentialHelperProviderTest.java new file mode 100644 index 00000000000000..25b6c03ff8e6b2 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/authandtls/credentialhelper/CredentialHelperProviderTest.java @@ -0,0 +1,376 @@ +// 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 com.google.common.truth.Truth8.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.base.Preconditions; +import com.google.devtools.build.lib.vfs.DigestHashFunction; +import com.google.devtools.build.lib.vfs.FileSystem; +import com.google.devtools.build.lib.vfs.Path; +import com.google.devtools.build.lib.vfs.PathFragment; +import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem; +import java.io.OutputStream; +import java.net.URI; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link CredentialHelperProvider}. */ +@RunWith(JUnit4.class) +public class CredentialHelperProviderTest { + private static final PathFragment DEFAULT_HELPER_PATH = + PathFragment.create("/path/to/default/helper"); + private static final PathFragment EXAMPLE_COM_HELPER_PATH = + PathFragment.create("/path/to/example/com/helper"); + private static final PathFragment EXAMPLE_COM_WILDCARD_HELPER_PATH = + PathFragment.create("/path/to/example/com/wildcard/helper"); + private static final PathFragment SUB_EXAMPLE_COM_WILDCARD_HELPER_PATH = + PathFragment.create("/path/to/sub/example/com/wildcard/helper"); + + private final FileSystem fileSystem = new InMemoryFileSystem(DigestHashFunction.SHA256); + + @Before + public void setUp() throws Exception { + setUpHelper(fileSystem.getPath(DEFAULT_HELPER_PATH)); + setUpHelper(fileSystem.getPath(EXAMPLE_COM_HELPER_PATH)); + setUpHelper(fileSystem.getPath(EXAMPLE_COM_WILDCARD_HELPER_PATH)); + setUpHelper(fileSystem.getPath(SUB_EXAMPLE_COM_WILDCARD_HELPER_PATH)); + } + + private void setUpHelper(Path path) throws Exception { + Preconditions.checkNotNull(path); + + path.getParentDirectory().createDirectoryAndParents(); + try (OutputStream stream = path.getOutputStream()) { + // Just create an empty file, nothing to do. + } + path.setExecutable(true); + } + + @Test + public void noHelpersConfigured() { + CredentialHelperProvider provider = CredentialHelperProvider.builder().build(); + + assertThat(provider.findCredentialHelper(URI.create("http://example.com/foo"))).isEmpty(); + assertThat(provider.findCredentialHelper(URI.create("https://example.com/foo"))).isEmpty(); + assertThat(provider.findCredentialHelper(URI.create("grpc://example.com/foo"))).isEmpty(); + assertThat(provider.findCredentialHelper(URI.create("grpcs://example.com/foo"))).isEmpty(); + assertThat(provider.findCredentialHelper(URI.create("custom://example.com/foo"))).isEmpty(); + + assertThat(provider.findCredentialHelper(URI.create("https://subdomain.example.com/bar"))) + .isEmpty(); + assertThat(provider.findCredentialHelper(URI.create("https://other-domain.com"))).isEmpty(); + } + + private void assertInvalidPattern(String pattern) { + Preconditions.checkNotNull(pattern); + + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> + CredentialHelperProvider.builder() + .add(pattern, fileSystem.getPath(DEFAULT_HELPER_PATH))); + assertThat(exception).hasMessageThat().contains(pattern); + } + + @Test + public void invalidPattern() throws Exception { + assertInvalidPattern("foo.*.example.com"); + assertInvalidPattern("*.foo.*.example.com"); + assertInvalidPattern("*-foo.example.com"); + assertInvalidPattern("example.*"); + assertInvalidPattern("*.example.*"); + + // Punycode + assertInvalidPattern("foo.*.münchen.de"); + assertInvalidPattern(".*.münchen.de"); + assertInvalidPattern("foo-*.münchen.de"); + } + + @Test + public void addNonExecutableDefaultHelper() throws Exception { + Path helper = fileSystem.getPath("/path/to/non/executable"); + setUpHelper(helper); + helper.setExecutable(false); + CredentialHelperProvider.Builder provider = CredentialHelperProvider.builder(); + + IllegalArgumentException exception = + assertThrows(IllegalArgumentException.class, () -> provider.add(helper)); + assertThat(exception).hasMessageThat().contains("is not executable"); + } + + @Test + public void onlyDefaultHelper() throws Exception { + Path helper = fileSystem.getPath(DEFAULT_HELPER_PATH); + CredentialHelperProvider provider = CredentialHelperProvider.builder().add(helper).build(); + + assertThat(provider.findCredentialHelper(URI.create("http://example.com/foo")).get().getPath()) + .isEqualTo(helper); + assertThat(provider.findCredentialHelper(URI.create("https://example.com/foo")).get().getPath()) + .isEqualTo(helper); + assertThat(provider.findCredentialHelper(URI.create("grpc://example.com/foo")).get().getPath()) + .isEqualTo(helper); + assertThat(provider.findCredentialHelper(URI.create("grpcs://example.com/foo")).get().getPath()) + .isEqualTo(helper); + assertThat( + provider.findCredentialHelper(URI.create("custom://example.com/foo")).get().getPath()) + .isEqualTo(helper); + + assertThat( + provider + .findCredentialHelper(URI.create("https://subdomain.example.com/bar")) + .get() + .getPath()) + .isEqualTo(helper); + assertThat( + provider.findCredentialHelper(URI.create("https://other-domain.com")).get().getPath()) + .isEqualTo(helper); + } + + @Test + public void withHostHelpersAndDefaultFallback() throws Exception { + Path defaultHelper = fileSystem.getPath(DEFAULT_HELPER_PATH); + Path exampleComHelper = fileSystem.getPath(EXAMPLE_COM_HELPER_PATH); + CredentialHelperProvider provider = + CredentialHelperProvider.builder() + .add(defaultHelper) + .add("example.com", exampleComHelper) + .build(); + + assertThat(provider.findCredentialHelper(URI.create("http://example.com/foo")).get().getPath()) + .isEqualTo(exampleComHelper); + assertThat(provider.findCredentialHelper(URI.create("https://example.com/foo")).get().getPath()) + .isEqualTo(exampleComHelper); + assertThat(provider.findCredentialHelper(URI.create("grpc://example.com/foo")).get().getPath()) + .isEqualTo(exampleComHelper); + assertThat(provider.findCredentialHelper(URI.create("grpcs://example.com/foo")).get().getPath()) + .isEqualTo(exampleComHelper); + assertThat( + provider.findCredentialHelper(URI.create("custom://example.com/foo")).get().getPath()) + .isEqualTo(exampleComHelper); + + assertThat( + provider + .findCredentialHelper(URI.create("https://subdomain.example.com/bar")) + .get() + .getPath()) + .isEqualTo(defaultHelper); + assertThat( + provider.findCredentialHelper(URI.create("https://other-domain.com")).get().getPath()) + .isEqualTo(defaultHelper); + } + + @Test + public void wildcardMatching() throws Exception { + Path defaultHelper = fileSystem.getPath(DEFAULT_HELPER_PATH); + Path exampleComWildcardHelper = fileSystem.getPath(EXAMPLE_COM_WILDCARD_HELPER_PATH); + CredentialHelperProvider provider = + CredentialHelperProvider.builder() + .add(defaultHelper) + .add("*.example.com", exampleComWildcardHelper) + .build(); + + assertThat(provider.findCredentialHelper(URI.create("http://example.com/foo")).get().getPath()) + .isEqualTo(exampleComWildcardHelper); + assertThat(provider.findCredentialHelper(URI.create("https://example.com/foo")).get().getPath()) + .isEqualTo(exampleComWildcardHelper); + assertThat(provider.findCredentialHelper(URI.create("grpc://example.com/foo")).get().getPath()) + .isEqualTo(exampleComWildcardHelper); + assertThat(provider.findCredentialHelper(URI.create("grpcs://example.com/foo")).get().getPath()) + .isEqualTo(exampleComWildcardHelper); + assertThat( + provider.findCredentialHelper(URI.create("custom://example.com/foo")).get().getPath()) + .isEqualTo(exampleComWildcardHelper); + + assertThat( + provider + .findCredentialHelper(URI.create("https://subdomain.example.com/bar")) + .get() + .getPath()) + .isEqualTo(exampleComWildcardHelper); + assertThat( + provider + .findCredentialHelper(URI.create("https://subdomain2.example.com/bar")) + .get() + .getPath()) + .isEqualTo(exampleComWildcardHelper); + assertThat( + provider + .findCredentialHelper(URI.create("https://sub.subdomain.example.com/bar")) + .get() + .getPath()) + .isEqualTo(exampleComWildcardHelper); + assertThat( + provider + .findCredentialHelper(URI.create("https://subdomain.example.com/bar")) + .get() + .getPath()) + .isEqualTo(exampleComWildcardHelper); + + assertThat( + provider.findCredentialHelper(URI.create("https://other-domain.com")).get().getPath()) + .isEqualTo(defaultHelper); + } + + @Test + public void preferExactMatchOverWildcardMatching() throws Exception { + Path defaultHelper = fileSystem.getPath(DEFAULT_HELPER_PATH); + Path exampleComHelper = fileSystem.getPath(EXAMPLE_COM_HELPER_PATH); + Path exampleComWildcardHelper = fileSystem.getPath(EXAMPLE_COM_WILDCARD_HELPER_PATH); + CredentialHelperProvider provider = + CredentialHelperProvider.builder() + .add(defaultHelper) + .add("example.com", exampleComHelper) + .add("*.example.com", exampleComWildcardHelper) + .build(); + + assertThat(provider.findCredentialHelper(URI.create("http://example.com/foo")).get().getPath()) + .isEqualTo(exampleComHelper); + assertThat(provider.findCredentialHelper(URI.create("https://example.com/foo")).get().getPath()) + .isEqualTo(exampleComHelper); + assertThat(provider.findCredentialHelper(URI.create("grpc://example.com/foo")).get().getPath()) + .isEqualTo(exampleComHelper); + assertThat(provider.findCredentialHelper(URI.create("grpcs://example.com/foo")).get().getPath()) + .isEqualTo(exampleComHelper); + assertThat( + provider.findCredentialHelper(URI.create("custom://example.com/foo")).get().getPath()) + .isEqualTo(exampleComHelper); + + assertThat( + provider + .findCredentialHelper(URI.create("https://subdomain.example.com/bar")) + .get() + .getPath()) + .isEqualTo(exampleComWildcardHelper); + assertThat( + provider + .findCredentialHelper(URI.create("https://subdomain2.example.com/bar")) + .get() + .getPath()) + .isEqualTo(exampleComWildcardHelper); + assertThat( + provider + .findCredentialHelper(URI.create("https://sub.subdomain.example.com/bar")) + .get() + .getPath()) + .isEqualTo(exampleComWildcardHelper); + assertThat( + provider + .findCredentialHelper(URI.create("https://subdomain.example.com/bar")) + .get() + .getPath()) + .isEqualTo(exampleComWildcardHelper); + + assertThat( + provider.findCredentialHelper(URI.create("https://other-domain.com")).get().getPath()) + .isEqualTo(defaultHelper); + } + + @Test + public void preferMostSpecificWildcardMatch() throws Exception { + Path exampleComWildcardHelper = fileSystem.getPath(EXAMPLE_COM_WILDCARD_HELPER_PATH); + Path subExampleComWildcardHelper = fileSystem.getPath(SUB_EXAMPLE_COM_WILDCARD_HELPER_PATH); + CredentialHelperProvider provider = + CredentialHelperProvider.builder() + .add("*.example.com", exampleComWildcardHelper) + .add("*.sub.example.com", subExampleComWildcardHelper) + .build(); + + assertThat(provider.findCredentialHelper(URI.create("https://example.com/bar")).get().getPath()) + .isEqualTo(exampleComWildcardHelper); + assertThat( + provider + .findCredentialHelper(URI.create("https://foo.example.com/bar")) + .get() + .getPath()) + .isEqualTo(exampleComWildcardHelper); + assertThat( + provider + .findCredentialHelper(URI.create("https://sub.example.com/bar")) + .get() + .getPath()) + .isEqualTo(subExampleComWildcardHelper); + assertThat( + provider + .findCredentialHelper(URI.create("https://foo.sub.example.com/bar")) + .get() + .getPath()) + .isEqualTo(subExampleComWildcardHelper); + } + + @Test + public void punycodeMatching() throws Exception { + Path defaultHelper = fileSystem.getPath(DEFAULT_HELPER_PATH); + Path specificHelper = fileSystem.getPath(EXAMPLE_COM_HELPER_PATH); + Path subdomainHelper = fileSystem.getPath(EXAMPLE_COM_HELPER_PATH); + + CredentialHelperProvider provider = + CredentialHelperProvider.builder() + .add(defaultHelper) + .add("münchen.de", specificHelper) + .add("*.köln.de", subdomainHelper) + .build(); + + // münchen.de + assertThat( + provider.findCredentialHelper(URI.create("http://xn--mnchen-3ya.de")).get().getPath()) + .isEqualTo(specificHelper); + assertThat( + provider + .findCredentialHelper(URI.create("http://foo.xn--mnchen-3ya.de")) + .get() + .getPath()) + .isEqualTo(defaultHelper); + assertThat(provider.findCredentialHelper(URI.create("http://muenchen.de")).get().getPath()) + .isEqualTo(defaultHelper); + + // köln.de + assertThat(provider.findCredentialHelper(URI.create("http://xn--kln-sna.de")).get().getPath()) + .isEqualTo(subdomainHelper); + assertThat( + provider.findCredentialHelper(URI.create("http://foo.xn--kln-sna.de")).get().getPath()) + .isEqualTo(subdomainHelper); + assertThat( + provider.findCredentialHelper(URI.create("http://bar.xn--kln-sna.de")).get().getPath()) + .isEqualTo(subdomainHelper); + assertThat(provider.findCredentialHelper(URI.create("http://koeln.de")).get().getPath()) + .isEqualTo(defaultHelper); + + // småland.se + assertThat( + provider.findCredentialHelper(URI.create("http://xn--smland-jua.se")).get().getPath()) + .isEqualTo(defaultHelper); + } + + @Test + public void parentDomain() { + assertThat(CredentialHelperProvider.parentDomain("com")).isEmpty(); + + assertThat(CredentialHelperProvider.parentDomain("foo.example.com")).hasValue("example.com"); + assertThat(CredentialHelperProvider.parentDomain("example.com")).hasValue("com"); + + // Punycode URIs (münchen.de). + assertThat(CredentialHelperProvider.parentDomain("foo.xn--mnchen-3ya.de")) + .hasValue("xn--mnchen-3ya.de"); + assertThat(CredentialHelperProvider.parentDomain("bar.foo.xn--mnchen-3ya.de")) + .hasValue("foo.xn--mnchen-3ya.de"); + assertThat(CredentialHelperProvider.parentDomain("xn--mnchen-3ya.de")).hasValue("de"); + } +}