Skip to content

Commit

Permalink
Add util for finding credential helper to use
Browse files Browse the repository at this point in the history
Progress on https://github.com/bazelbuild/proposals/blob/main/designs/2022-06-07-bazel-credential-helpers.md

Closes bazelbuild#15707.

PiperOrigin-RevId: 458456496
Change-Id: I751a594144c3563096ee9794c41329b49755824e
  • Loading branch information
Yannic authored and copybara-github committed Jul 1, 2022
1 parent 255e8f5 commit 4175018
Show file tree
Hide file tree
Showing 7 changed files with 665 additions and 2 deletions.
4 changes: 3 additions & 1 deletion src/main/java/com/google/devtools/build/lib/authandtls/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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__"],
)

Expand Down
Original file line number Diff line number Diff line change
@@ -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",
],
)
Original file line number Diff line number Diff line change
@@ -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.
}
Original file line number Diff line number Diff line change
@@ -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.
*
* <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 = Preconditions.checkNotNull(uri.getHost());
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));
}
}
}
4 changes: 3 additions & 1 deletion src/test/java/com/google/devtools/build/lib/authandtls/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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__"],
)

Expand Down
Original file line number Diff line number Diff line change
@@ -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",
],
)
Loading

0 comments on commit 4175018

Please sign in to comment.