| // 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.base.Strings; |
| 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. |
| * |
| * <p>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<Path> defaultHelper; |
| |
| @SuppressWarnings("Immutable") |
| private final ImmutableMap<String, Path> hostToHelper; |
| |
| @SuppressWarnings("Immutable") |
| private final ImmutableMap<String, Path> suffixToHelper; |
| |
| private CredentialHelperProvider( |
| Optional<Path> defaultHelper, |
| ImmutableMap<String, Path> hostToHelper, |
| ImmutableMap<String, Path> 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<CredentialHelper> findCredentialHelper(URI uri) { |
| Preconditions.checkNotNull(uri); |
| |
| String host = uri.getHost(); |
| if (Strings.isNullOrEmpty(host)) { |
| // Some URIs (e.g. unix://) legitimately have no host component. |
| return Optional.empty(); |
| } |
| |
| Optional<Path> credentialHelper = |
| findHostCredentialHelper(host) |
| .or(() -> findWildcardCredentialHelper(host)) |
| .or(() -> defaultHelper); |
| return credentialHelper.map(CredentialHelper::new); |
| } |
| |
| private Optional<Path> findHostCredentialHelper(String host) { |
| Preconditions.checkNotNull(host); |
| |
| return Optional.ofNullable(hostToHelper.get(host)); |
| } |
| |
| private Optional<Path> findWildcardCredentialHelper(String host) { |
| Preconditions.checkNotNull(host); |
| |
| return Optional.ofNullable(suffixToHelper.get(host)) |
| .or( |
| () -> { |
| Optional<String> 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<String> 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<Path> defaultHelper = Optional.empty(); |
| private final Map<String, Path> hostToHelper = new HashMap<>(); |
| private final Map<String, Path> 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. |
| * |
| * <p>As of 2022-06-20, only matching based on (wildcard) domain name is supported. |
| * |
| * <p>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)); |
| } |
| } |
| } |