mirror of https://github.com/grpc/grpc-java.git
core: SpiffeId parser (#11490)
SpiffeId parser compliant with [official spec](https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE-ID.md)
This commit is contained in:
parent
64e3801538
commit
1c069375ce
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* Copyright 2024 The gRPC Authors
|
||||
*
|
||||
* 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 io.grpc.internal;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import com.google.common.base.Splitter;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Helper utility to work with SPIFFE URIs.
|
||||
* @see <a href="https://github.com/spiffe/spiffe/blob/master/standards/SPIFFE-ID.md">Standard</a>
|
||||
*/
|
||||
public final class SpiffeUtil {
|
||||
|
||||
private static final String PREFIX = "spiffe://";
|
||||
|
||||
private SpiffeUtil() {}
|
||||
|
||||
/**
|
||||
* Parses a URI string, applies validation rules described in SPIFFE standard, and, in case of
|
||||
* success, returns parsed TrustDomain and Path.
|
||||
*
|
||||
* @param uri a String representing a SPIFFE ID
|
||||
*/
|
||||
public static SpiffeId parse(String uri) {
|
||||
doInitialUriValidation(uri);
|
||||
checkArgument(uri.toLowerCase(Locale.US).startsWith(PREFIX), "Spiffe Id must start with "
|
||||
+ PREFIX);
|
||||
String domainAndPath = uri.substring(PREFIX.length());
|
||||
String trustDomain;
|
||||
String path;
|
||||
if (!domainAndPath.contains("/")) {
|
||||
trustDomain = domainAndPath;
|
||||
path = "";
|
||||
} else {
|
||||
String[] parts = domainAndPath.split("/", 2);
|
||||
trustDomain = parts[0];
|
||||
path = parts[1];
|
||||
checkArgument(!path.isEmpty(), "Path must not include a trailing '/'");
|
||||
}
|
||||
validateTrustDomain(trustDomain);
|
||||
validatePath(path);
|
||||
if (!path.isEmpty()) {
|
||||
path = "/" + path;
|
||||
}
|
||||
return new SpiffeId(trustDomain, path);
|
||||
}
|
||||
|
||||
private static void doInitialUriValidation(String uri) {
|
||||
checkArgument(checkNotNull(uri, "uri").length() > 0, "Spiffe Id can't be empty");
|
||||
checkArgument(uri.length() <= 2048, "Spiffe Id maximum length is 2048 characters");
|
||||
checkArgument(!uri.contains("#"), "Spiffe Id must not contain query fragments");
|
||||
checkArgument(!uri.contains("?"), "Spiffe Id must not contain query parameters");
|
||||
}
|
||||
|
||||
private static void validateTrustDomain(String trustDomain) {
|
||||
checkArgument(!trustDomain.isEmpty(), "Trust Domain can't be empty");
|
||||
checkArgument(trustDomain.length() < 256, "Trust Domain maximum length is 255 characters");
|
||||
checkArgument(trustDomain.matches("[a-z0-9._-]+"),
|
||||
"Trust Domain must contain only letters, numbers, dots, dashes, and underscores"
|
||||
+ " ([a-z0-9.-_])");
|
||||
}
|
||||
|
||||
private static void validatePath(String path) {
|
||||
if (path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
checkArgument(!path.endsWith("/"), "Path must not include a trailing '/'");
|
||||
for (String segment : Splitter.on("/").split(path)) {
|
||||
validatePathSegment(segment);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validatePathSegment(String pathSegment) {
|
||||
checkArgument(!pathSegment.isEmpty(), "Individual path segments must not be empty");
|
||||
checkArgument(!(pathSegment.equals(".") || pathSegment.equals("..")),
|
||||
"Individual path segments must not be relative path modifiers (i.e. ., ..)");
|
||||
checkArgument(pathSegment.matches("[a-zA-Z0-9._-]+"),
|
||||
"Individual path segments must contain only letters, numbers, dots, dashes, and underscores"
|
||||
+ " ([a-zA-Z0-9.-_])");
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a SPIFFE ID as defined in the SPIFFE standard.
|
||||
* @see <a href="https://github.com/spiffe/spiffe/blob/master/standards/SPIFFE-ID.md">Standard</a>
|
||||
*/
|
||||
public static class SpiffeId {
|
||||
|
||||
private final String trustDomain;
|
||||
private final String path;
|
||||
|
||||
private SpiffeId(String trustDomain, String path) {
|
||||
this.trustDomain = trustDomain;
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public String getTrustDomain() {
|
||||
return trustDomain;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
/*
|
||||
* Copyright 2024 The gRPC Authors
|
||||
*
|
||||
* 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 io.grpc.internal;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import org.junit.Test;
|
||||
import org.junit.experimental.runners.Enclosed;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.junit.runners.Parameterized.Parameter;
|
||||
import org.junit.runners.Parameterized.Parameters;
|
||||
|
||||
|
||||
@RunWith(Enclosed.class)
|
||||
public class SpiffeUtilTest {
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public static class ParseSuccessTest {
|
||||
@Parameter
|
||||
public String uri;
|
||||
|
||||
@Parameter(1)
|
||||
public String trustDomain;
|
||||
|
||||
@Parameter(2)
|
||||
public String path;
|
||||
|
||||
@Test
|
||||
public void parseSuccessTest() {
|
||||
SpiffeUtil.SpiffeId spiffeId = SpiffeUtil.parse(uri);
|
||||
assertEquals(trustDomain, spiffeId.getTrustDomain());
|
||||
assertEquals(path, spiffeId.getPath());
|
||||
}
|
||||
|
||||
@Parameters(name = "spiffeId={0}")
|
||||
public static Collection<String[]> data() {
|
||||
return Arrays.asList(new String[][] {
|
||||
{"spiffe://example.com", "example.com", ""},
|
||||
{"spiffe://example.com/us", "example.com", "/us"},
|
||||
{"spIFfe://qa-staging.final_check.example.com/us", "qa-staging.final_check.example.com",
|
||||
"/us"},
|
||||
{"spiffe://example.com/country/us/state/FL/city/Miami", "example.com",
|
||||
"/country/us/state/FL/city/Miami"},
|
||||
{"SPIFFE://example.com/Czech.Republic/region0.1/city_of-Prague", "example.com",
|
||||
"/Czech.Republic/region0.1/city_of-Prague"},
|
||||
{"spiffe://trust-domain-name/path", "trust-domain-name", "/path"},
|
||||
{"spiffe://staging.example.com/payments/mysql", "staging.example.com", "/payments/mysql"},
|
||||
{"spiffe://staging.example.com/payments/web-fe", "staging.example.com",
|
||||
"/payments/web-fe"},
|
||||
{"spiffe://k8s-west.example.com/ns/staging/sa/default", "k8s-west.example.com",
|
||||
"/ns/staging/sa/default"},
|
||||
{"spiffe://example.com/9eebccd2-12bf-40a6-b262-65fe0487d453", "example.com",
|
||||
"/9eebccd2-12bf-40a6-b262-65fe0487d453"},
|
||||
{"spiffe://trustdomain/.a..", "trustdomain", "/.a.."},
|
||||
{"spiffe://trustdomain/...", "trustdomain", "/..."},
|
||||
{"spiffe://trustdomain/abcdefghijklmnopqrstuvwxyz", "trustdomain",
|
||||
"/abcdefghijklmnopqrstuvwxyz"},
|
||||
{"spiffe://trustdomain/abc0123.-_", "trustdomain", "/abc0123.-_"},
|
||||
{"spiffe://trustdomain/0123456789", "trustdomain", "/0123456789"},
|
||||
{"spiffe://trustdomain0123456789/path", "trustdomain0123456789", "/path"},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public static class ParseFailureTest {
|
||||
@Parameter
|
||||
public String uri;
|
||||
|
||||
@Test
|
||||
public void parseFailureTest() {
|
||||
assertThrows(IllegalArgumentException.class, () -> SpiffeUtil.parse(uri));
|
||||
}
|
||||
|
||||
@Parameters(name = "spiffeId={0}")
|
||||
public static Collection<String> data() {
|
||||
return Arrays.asList(
|
||||
"spiffe:///",
|
||||
"spiffe://example!com",
|
||||
"spiffe://exampleя.com/workload-1",
|
||||
"spiffe://example.com/us/florida/miamiя",
|
||||
"spiffe:/trustdomain/path",
|
||||
"spiffe:///path",
|
||||
"spiffe://trust%20domain/path",
|
||||
"spiffe://user@trustdomain/path",
|
||||
"spiffe:// /",
|
||||
"",
|
||||
"http://trustdomain/path",
|
||||
"//trustdomain/path",
|
||||
"://trustdomain/path",
|
||||
"piffe://trustdomain/path",
|
||||
"://",
|
||||
"://trustdomain",
|
||||
"spiff",
|
||||
"spiffe",
|
||||
"spiffe:////",
|
||||
"spiffe://trust.domain/../path"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ExceptionMessageTest {
|
||||
|
||||
@Test
|
||||
public void spiffeUriFormatTest() {
|
||||
NullPointerException npe = assertThrows(NullPointerException.class, () ->
|
||||
SpiffeUtil.parse(null));
|
||||
assertEquals("uri", npe.getMessage());
|
||||
|
||||
IllegalArgumentException iae = assertThrows(IllegalArgumentException.class, () ->
|
||||
SpiffeUtil.parse("https://example.com"));
|
||||
assertEquals("Spiffe Id must start with spiffe://", iae.getMessage());
|
||||
|
||||
iae = assertThrows(IllegalArgumentException.class, () ->
|
||||
SpiffeUtil.parse("spiffe://example.com/workload#1"));
|
||||
assertEquals("Spiffe Id must not contain query fragments", iae.getMessage());
|
||||
|
||||
iae = assertThrows(IllegalArgumentException.class, () ->
|
||||
SpiffeUtil.parse("spiffe://example.com/workload-1?t=1"));
|
||||
assertEquals("Spiffe Id must not contain query parameters", iae.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void spiffeTrustDomainFormatTest() {
|
||||
IllegalArgumentException iae = assertThrows(IllegalArgumentException.class, () ->
|
||||
SpiffeUtil.parse("spiffe://"));
|
||||
assertEquals("Trust Domain can't be empty", iae.getMessage());
|
||||
|
||||
iae = assertThrows(IllegalArgumentException.class, () ->
|
||||
SpiffeUtil.parse("spiffe://eXample.com"));
|
||||
assertEquals(
|
||||
"Trust Domain must contain only letters, numbers, dots, dashes, and underscores "
|
||||
+ "([a-z0-9.-_])",
|
||||
iae.getMessage());
|
||||
|
||||
StringBuilder longTrustDomain = new StringBuilder("spiffe://pi.eu.");
|
||||
for (int i = 0; i < 50; i++) {
|
||||
longTrustDomain.append("pi.eu");
|
||||
}
|
||||
iae = assertThrows(IllegalArgumentException.class, () ->
|
||||
SpiffeUtil.parse(longTrustDomain.toString()));
|
||||
assertEquals("Trust Domain maximum length is 255 characters", iae.getMessage());
|
||||
|
||||
StringBuilder longSpiffe = new StringBuilder(String.format("spiffe://mydomain%scom/", "%21"));
|
||||
for (int i = 0; i < 405; i++) {
|
||||
longSpiffe.append("qwert");
|
||||
}
|
||||
iae = assertThrows(IllegalArgumentException.class, () ->
|
||||
SpiffeUtil.parse(longSpiffe.toString()));
|
||||
assertEquals("Spiffe Id maximum length is 2048 characters", iae.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void spiffePathFormatTest() {
|
||||
IllegalArgumentException iae = assertThrows(IllegalArgumentException.class, () ->
|
||||
SpiffeUtil.parse("spiffe://example.com//"));
|
||||
assertEquals("Path must not include a trailing '/'", iae.getMessage());
|
||||
|
||||
iae = assertThrows(IllegalArgumentException.class, () ->
|
||||
SpiffeUtil.parse("spiffe://example.com/"));
|
||||
assertEquals("Path must not include a trailing '/'", iae.getMessage());
|
||||
|
||||
iae = assertThrows(IllegalArgumentException.class, () ->
|
||||
SpiffeUtil.parse("spiffe://example.com/us//miami"));
|
||||
assertEquals("Individual path segments must not be empty", iae.getMessage());
|
||||
|
||||
iae = assertThrows(IllegalArgumentException.class, () ->
|
||||
SpiffeUtil.parse("spiffe://example.com/us/."));
|
||||
assertEquals("Individual path segments must not be relative path modifiers (i.e. ., ..)",
|
||||
iae.getMessage());
|
||||
|
||||
iae = assertThrows(IllegalArgumentException.class, () ->
|
||||
SpiffeUtil.parse("spiffe://example.com/us!"));
|
||||
assertEquals("Individual path segments must contain only letters, numbers, dots, dashes, and "
|
||||
+ "underscores ([a-zA-Z0-9.-_])", iae.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue