From f823dbb8393a4e076171e58c2c7bfd6865e09c98 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Wed, 19 Jun 2019 16:55:58 -0400 Subject: [PATCH 01/12] Nothing uses RegistryCredentials --- .../credentials/RegistryCredentials.java | 48 ------------------- 1 file changed, 48 deletions(-) delete mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/RegistryCredentials.java diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/RegistryCredentials.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/RegistryCredentials.java deleted file mode 100644 index e2fc2ccb14..0000000000 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/credentials/RegistryCredentials.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2018 Google LLC. - * - * 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.cloud.tools.jib.registry.credentials; - -import com.google.cloud.tools.jib.http.Authorization; - -/** - * Stores retrieved registry credentials and their source. - * - *

The credentials are referred to by the registry they are used for. - */ -public class RegistryCredentials { - - private final Authorization authorization; - - /** - * A string representation of where the credentials were retrieved from. This is useful for - * letting the user know which credentials were used. - */ - private final String credentialSource; - - public RegistryCredentials(String credentialSource, Authorization authorization) { - this.authorization = authorization; - this.credentialSource = credentialSource; - } - - public Authorization getAuthorization() { - return authorization; - } - - public String getCredentialSource() { - return credentialSource; - } -} From 8f5fef6e035b7a771849c8d61fb203cf44193be4 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Wed, 19 Jun 2019 22:29:11 -0400 Subject: [PATCH 02/12] Enable cross-repository blob mounts --- jib-core/build.gradle | 1 + .../tools/jib/builder/steps/PushBlobStep.java | 19 ++- .../jib/configuration/BuildConfiguration.java | 12 ++ .../cloud/tools/jib/http/Authorization.java | 90 ++++++++++++- .../jib/registry/RegistryAuthenticator.java | 62 ++++++--- .../tools/jib/registry/RegistryClient.java | 14 +- .../RegistryEndpointRequestProperties.java | 19 +++ .../tools/jib/http/AuthorizationTest.java | 124 ++++++++++++++++++ .../AuthenticationMethodRetrieverTest.java | 4 +- .../registry/RegistryAuthenticatorTest.java | 21 ++- jib-maven-plugin/pom.xml | 6 + 11 files changed, 340 insertions(+), 32 deletions(-) create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/http/AuthorizationTest.java diff --git a/jib-core/build.gradle b/jib-core/build.gradle index a7808dd240..c43a1f5e39 100644 --- a/jib-core/build.gradle +++ b/jib-core/build.gradle @@ -40,6 +40,7 @@ dependencies { implementation 'com.google.guava:guava:27.0.1-jre' implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.9' implementation 'org.ow2.asm:asm:7.0' + implementation 'com.auth0:java-jwt:3.8.1' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:2.23.4' diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java index 662b37a3cd..c8d7e4dc35 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java @@ -27,6 +27,7 @@ import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.event.progress.ThrottledAccumulatingConsumer; +import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; @@ -82,10 +83,11 @@ public BlobDescriptor call() throws IOException, RegistryException, ExecutionExc buildConfiguration.getEventHandlers(), DESCRIPTION + blobDescriptor); ThrottledAccumulatingConsumer throttledProgressReporter = new ThrottledAccumulatingConsumer(progressEventDispatcher::dispatchProgress)) { + Authorization authorization = NonBlockingSteps.get(authenticatePushStep); RegistryClient registryClient = buildConfiguration .newTargetImageRegistryClientFactory() - .setAuthorization(NonBlockingSteps.get(authenticatePushStep)) + .setAuthorization(authorization) .newRegistryClient(); // check if the BLOB is available @@ -96,8 +98,19 @@ public BlobDescriptor call() throws IOException, RegistryException, ExecutionExc return blobDescriptor; } - // todo: leverage cross-repository mounts - registryClient.pushBlob(blobDescriptor.getDigest(), blob, null, throttledProgressReporter); + // If base and target images are in the same registry, then use mount/from to try mounting the + // BLOB from the base image repository to the target image repository and possibly avoid + // having to push the BLOB. See + // https://docs.docker.com/registry/spec/api/#cross-repository-blob-mount for details. + String baseRegistry = buildConfiguration.getBaseImageConfiguration().getImageRegistry(); + String baseRepository = buildConfiguration.getBaseImageConfiguration().getImageRepository(); + String targetRegistry = buildConfiguration.getTargetImageConfiguration().getImageRegistry(); + String sourceRepository = + targetRegistry.equals(baseRegistry) && authorization.canAccess(baseRepository, "pull") + ? baseRepository + : null; + registryClient.pushBlob( + blobDescriptor.getDigest(), blob, sourceRepository, throttledProgressReporter); return blobDescriptor; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java index 1c9ca20b53..e4d102d5bd 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/configuration/BuildConfiguration.java @@ -449,6 +449,18 @@ public RegistryClient.Factory newBaseImageRegistryClientFactory() { * @return a new {@link RegistryClient.Factory} */ public RegistryClient.Factory newTargetImageRegistryClientFactory() { + // if base and target are on the same registry, try enabling cross-repository mounts + if (baseImageConfiguration + .getImageRegistry() + .equals(targetImageConfiguration.getImageRegistry())) { + return RegistryClient.factory( + getEventHandlers(), + targetImageConfiguration.getImageRegistry(), + targetImageConfiguration.getImageRepository(), + baseImageConfiguration.getImageRepository()) + .setAllowInsecureRegistries(getAllowInsecureRegistries()) + .setUserAgentSuffix(getToolName()); + } return newRegistryClientFactory(targetImageConfiguration); } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java index 7c764f4f2f..6bdc13844f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java @@ -16,9 +16,21 @@ package com.google.cloud.tools.jib.http; +import com.auth0.jwt.JWT; +import com.auth0.jwt.exceptions.JWTDecodeException; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.fasterxml.jackson.databind.JsonNode; import com.google.api.client.util.Base64; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableSetMultimap; +import com.google.common.collect.Iterators; +import com.google.common.collect.Multimap; +import com.google.common.collect.Streams; import java.nio.charset.StandardCharsets; import java.util.Objects; +import java.util.stream.Stream; +import javax.annotation.Nullable; /** * Holds the credentials for an HTTP {@code Authorization} header. @@ -34,9 +46,65 @@ public class Authorization { * @return an {@link Authorization} with a {@code Bearer} token */ public static Authorization fromBearerToken(String token) { - return new Authorization("Bearer", token); + return new Authorization("Bearer", token, decodeTokenRepositoryGrants(token)); } + /** + * Decode the Docker Registry v2 Bearer + * Token to list the granted repositories with their levels of access. + * + * @param token a Docker Registry Bearer Token + * @return a mapping of repository to granted access scopes, or {@code null} if there is no JWT + * the token is not a Docker Registry Bearer Token + */ + @VisibleForTesting + static Multimap decodeTokenRepositoryGrants(String token) { + // Docker Registry Bearer Tokens are based on JWT. The payload looks like: + // { + // "access":[{"type":"repository","name":"repository/name","actions":["pull"]}], + // "aud":"registry.docker.io", + // "iss":"auth.docker.io", + // "exp":999, + // "iat":999, + // "jti":"zzzz", + // "nbf":999, + // "sub":"e3ae001d-xxx" + // } + try { + DecodedJWT jwt = JWT.decode(token); + // Make sure they look like valid access claims. + JsonNode[] accessClaims = jwt.getClaim("access").asArray(JsonNode.class); + if (accessClaims == null + || !Stream.of(accessClaims).allMatch(Authorization::isValidAccessClaim)) { + return null; + } + return Stream.of(accessClaims) + .filter(n -> "repository".equals(n.get("type").asText())) + .collect( + ImmutableSetMultimap.flatteningToImmutableSetMultimap( + n -> n.get("name").asText(), + n -> Streams.stream(n.get("actions").iterator()).map(JsonNode::asText))); + } catch (JWTDecodeException exception) { + return null; + } + } + + /** + * Check that the provided access object looks genuine: should be a JSON object with a non-empty + * string "type" field. + */ + private static boolean isValidAccessClaim(JsonNode n) { + if (!n.isObject() || Strings.isNullOrEmpty(n.get("type").asText())) { + return false; + } + if ("repository".equals(n.get("type").asText())) { + // repository should have a name and array of permitted actions + return !Strings.isNullOrEmpty(n.get("name").asText()) + && n.get("actions").isArray() + && Iterators.all(n.get("actions").iterator(), JsonNode::isTextual); + } + return true; + } /** * @param username the username * @param secret the secret @@ -45,7 +113,7 @@ public static Authorization fromBearerToken(String token) { public static Authorization fromBasicCredentials(String username, String secret) { String credentials = username + ":" + secret; String token = Base64.encodeBase64String(credentials.getBytes(StandardCharsets.UTF_8)); - return new Authorization("Basic", token); + return new Authorization("Basic", token, null); } /** @@ -53,15 +121,17 @@ public static Authorization fromBasicCredentials(String username, String secret) * @return an {@link Authorization} with a base64-encoded {@code username:password} string */ public static Authorization fromBasicToken(String token) { - return new Authorization("Basic", token); + return new Authorization("Basic", token, null); } private final String scheme; private final String token; + @Nullable private final Multimap repositoryGrants; - private Authorization(String scheme, String token) { + private Authorization(String scheme, String token, Multimap repositoryGrants) { this.scheme = scheme; this.token = token; + this.repositoryGrants = repositoryGrants; } public String getScheme() { @@ -94,4 +164,16 @@ public boolean equals(Object other) { public int hashCode() { return Objects.hash(scheme, token); } + + /** + * Check if this authorization allows accessing the specified repository. + * + * @param repository repository in question + * @param access the access scope ("push" or "pull") + * @return true if the repository was covered + */ + public boolean canAccess(String repository, String access) { + // if null then we assume that all repositories are granted + return repositoryGrants == null || repositoryGrants.containsEntry(repository, access); + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java index f67f08644a..9dadb82bc2 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java @@ -30,6 +30,7 @@ import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Verify; +import com.google.common.collect.ImmutableMap; import com.google.common.io.CharStreams; import com.google.common.net.MediaType; import java.io.IOException; @@ -37,6 +38,8 @@ import java.net.MalformedURLException; import java.net.URL; import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.Map.Entry; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.Nullable; @@ -180,26 +183,29 @@ public Authorization authenticatePush(@Nullable Credential credential) } @VisibleForTesting - String getServiceScopeRequestParameters(String scope) { - return "service=" - + service - + "&scope=repository:" - + registryEndpointRequestProperties.getImageName() - + ":" - + scope; + String getServiceScopeRequestParameters(Map scopes) { + StringBuilder parameters = new StringBuilder("service=").append(service); + for (Entry pair : scopes.entrySet()) { + parameters + .append("&scope=repository:") + .append(pair.getKey()) + .append(":") + .append(pair.getValue()); + } + return parameters.toString(); } @VisibleForTesting - URL getAuthenticationUrl(@Nullable Credential credential, String scope) + URL getAuthenticationUrl(@Nullable Credential credential, Map scopes) throws MalformedURLException { return isOAuth2Auth(credential) ? new URL(realm) // Required parameters will be sent via POST . - : new URL(realm + "?" + getServiceScopeRequestParameters(scope)); + : new URL(realm + "?" + getServiceScopeRequestParameters(scopes)); } @VisibleForTesting - String getAuthRequestParameters(@Nullable Credential credential, String scope) { - String serviceScope = getServiceScopeRequestParameters(scope); + String getAuthRequestParameters(@Nullable Credential credential, Map scopes) { + String serviceScope = getServiceScopeRequestParameters(scopes); return isOAuth2Auth(credential) ? serviceScope // https://github.com/GoogleContainerTools/jib/pull/1545 @@ -227,15 +233,41 @@ boolean isOAuth2Auth(@Nullable Credential credential) { */ private Authorization authenticate(@Nullable Credential credential, String scope) throws RegistryAuthenticationFailedException { + // try authorizing againat both the main repository and the source repository too + // to enable cross-repository mounts on pushes + if (registryEndpointRequestProperties.getSourceImageName() != null) { + try { + Map scopes = + ImmutableMap.of( + registryEndpointRequestProperties.getImageName(), + scope, + registryEndpointRequestProperties.getSourceImageName(), + "pull"); + Authorization auth = authenticate(credential, scopes); + if (auth != null) { + return auth; + } + } catch (RegistryAuthenticationFailedException ex) { + // Unable to obtain authorization with source image: fallthrough and try without + } + } + Map scopes = + ImmutableMap.of(registryEndpointRequestProperties.getImageName(), scope); + Authorization auth = authenticate(credential, scopes); + return auth; + } + + private Authorization authenticate(@Nullable Credential credential, Map scopes) + throws RegistryAuthenticationFailedException { try (Connection connection = - Connection.getConnectionFactory().apply(getAuthenticationUrl(credential, scope))) { + Connection.getConnectionFactory().apply(getAuthenticationUrl(credential, scopes))) { Request.Builder requestBuilder = Request.builder() .setHttpTimeout(JibSystemProperties.getHttpTimeout()) .setUserAgent(userAgent); if (isOAuth2Auth(credential)) { - String parameters = getAuthRequestParameters(credential, scope); + String parameters = getAuthRequestParameters(credential, scopes); requestBuilder.setBody( new BlobHttpContent(Blobs.from(parameters), MediaType.FORM_DATA.toString())); } else if (credential != null) { @@ -257,9 +289,9 @@ private Authorization authenticate(@Nullable Credential credential, String scope registryEndpointRequestProperties.getServerUrl(), registryEndpointRequestProperties.getImageName(), "Did not get token in authentication response from " - + getAuthenticationUrl(credential, scope) + + getAuthenticationUrl(credential, scopes) + "; parameters: " - + getAuthRequestParameters(credential, scope)); + + getAuthRequestParameters(credential, scopes)); } return Authorization.fromBearerToken(responseJson.getToken()); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java index b1b7e9e2c9..d53b8c923c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java @@ -138,7 +138,17 @@ private String makeUserAgent() { * @return the new {@link Factory} */ public static Factory factory(EventHandlers eventHandlers, String serverUrl, String imageName) { - return new Factory(eventHandlers, new RegistryEndpointRequestProperties(serverUrl, imageName)); + return factory(eventHandlers, serverUrl, imageName, null); + } + + public static Factory factory( + EventHandlers eventHandlers, + String serverUrl, + String imageName, + @Nullable String sourceImageName) { + return new Factory( + eventHandlers, + new RegistryEndpointRequestProperties(serverUrl, imageName, sourceImageName)); } private final EventHandlers eventHandlers; @@ -298,8 +308,8 @@ public boolean pushBlob( try (TimerEventDispatcher timerEventDispatcher2 = timerEventDispatcher.subTimer("pushBlob POST " + blobDigest)) { - // POST /v2//blobs/uploads/ OR // POST /v2//blobs/uploads/?mount={blob.digest}&from={sourceRepository} + // POST /v2//blobs/uploads/ URL patchLocation = callRegistryEndpoint(blobPusher.initializer()); if (patchLocation == null) { // The BLOB exists already. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointRequestProperties.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointRequestProperties.java index 4483344f74..15c15c5202 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointRequestProperties.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointRequestProperties.java @@ -16,19 +16,33 @@ package com.google.cloud.tools.jib.registry; +import javax.annotation.Nullable; + /** Properties of registry endpoint requests. */ class RegistryEndpointRequestProperties { private final String serverUrl; private final String imageName; + @Nullable private final String sourceImageName; /** * @param serverUrl the server URL for the registry (for example, {@code gcr.io}) * @param imageName the image/repository name (also known as, namespace) */ RegistryEndpointRequestProperties(String serverUrl, String imageName) { + this(serverUrl, imageName, null); + } + + /** + * @param serverUrl the server URL for the registry (for example, {@code gcr.io}) + * @param imageName the image/repository name (also known as, namespace) + * @param sourceImageName additional source image on the registry + */ + RegistryEndpointRequestProperties( + String serverUrl, String imageName, @Nullable String sourceImageName) { this.serverUrl = serverUrl; this.imageName = imageName; + this.sourceImageName = sourceImageName; } String getServerUrl() { @@ -38,4 +52,9 @@ String getServerUrl() { String getImageName() { return imageName; } + + @Nullable + String getSourceImageName() { + return sourceImageName; + } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/AuthorizationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/AuthorizationTest.java new file mode 100644 index 0000000000..8a94a1b65e --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/AuthorizationTest.java @@ -0,0 +1,124 @@ +/* + * Copyright 2019 Google Inc. + * + * 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.cloud.tools.jib.http; + +import com.google.common.collect.Multimap; +import org.junit.Assert; +import org.junit.Test; + +/** Tests for {@link Authorization}. */ +public class AuthorizationTest { + @Test + public void testDecode_dockerToken() { + // a genuine token for accessing docker.io's openjdk + Multimap decoded = + Authorization.decodeTokenRepositoryGrants( + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsIng1YyI6WyJNSUlDK2pDQ0FwK2dBd0lCQWdJQkFEQUtCZ2dxaGtqT1BRUURBakJHTVVRd1FnWURWUVFERXpzeVYwNVpPbFZMUzFJNlJFMUVVanBTU1U5Rk9reEhOa0U2UTFWWVZEcE5SbFZNT2tZelNFVTZOVkF5VlRwTFNqTkdPa05CTmxrNlNrbEVVVEFlRncweE9UQXhNVEl3TURJeU5EVmFGdzB5TURBeE1USXdNREl5TkRWYU1FWXhSREJDQmdOVkJBTVRPMUpMTkZNNlMwRkxVVHBEV0RWRk9rRTJSMVE2VTBwTVR6cFFNbEpMT2tOWlZVUTZTMEpEU0RwWFNVeE1Pa3hUU2xrNldscFFVVHBaVWxsRU1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcjY2bXkveXpHN21VUzF3eFQ3dFplS2pqRzcvNnBwZFNMY3JCcko5VytwcndzMGtIUDVwUHRkMUpkcFdEWU1OZWdqQXhpUWtRUUNvd25IUnN2ODVUalBUdE5wUkdKVTRkeHJkeXBvWGc4TVhYUEUzL2lRbHhPS2VNU0prNlRKbG5wNGFtWVBHQlhuQXRoQzJtTlR5ak1zdFh2ZmNWN3VFYWpRcnlOVUcyUVdXQ1k1Ujl0a2k5ZG54Z3dCSEF6bG8wTzJCczFmcm5JbmJxaCtic3ZSZ1FxU3BrMWhxYnhSU3AyRlNrL2tBL1gyeUFxZzJQSUJxWFFMaTVQQ3krWERYZElJczV6VG9ZbWJUK0pmbnZaMzRLcG5mSkpNalpIRW4xUVJtQldOZXJZcVdtNVhkQVhUMUJrQU9aditMNFVwSTk3NFZFZ2ppY1JINVdBeWV4b1BFclRRSURBUUFCbzRHeU1JR3ZNQTRHQTFVZER3RUIvd1FFQXdJSGdEQVBCZ05WSFNVRUNEQUdCZ1JWSFNVQU1FUUdBMVVkRGdROUJEdFNTelJUT2t0QlMxRTZRMWcxUlRwQk5rZFVPbE5LVEU4NlVESlNTenBEV1ZWRU9rdENRMGc2VjBsTVREcE1VMHBaT2xwYVVGRTZXVkpaUkRCR0JnTlZIU01FUHpBOWdEc3lWMDVaT2xWTFMxSTZSRTFFVWpwU1NVOUZPa3hITmtFNlExVllWRHBOUmxWTU9rWXpTRVU2TlZBeVZUcExTak5HT2tOQk5sazZTa2xFVVRBS0JnZ3Foa2pPUFFRREFnTkpBREJHQWlFQXFOSXEwMFdZTmM5Z2tDZGdSUzRSWUhtNTRZcDBTa05Rd2lyMm5hSWtGd3dDSVFEMjlYdUl5TmpTa1cvWmpQaFlWWFB6QW9TNFVkRXNvUUhyUVZHMDd1N3ZsUT09Il19" + + ".eyJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6ImxpYnJhcnkvb3BlbmpkayIsImFjdGlvbnMiOlsicHVsbCJdfV0sImF1ZCI6InJlZ2lzdHJ5LmRvY2tlci5pbyIsImV4cCI6MTU2MTA0MzkwNSwiaWF0IjoxNTYxMDQzNjA1LCJpc3MiOiJhdXRoLmRvY2tlci5pbyIsImp0aSI6Ikc5bWpiOE9GeU5STFlpY3ZUMFZxIiwibmJmIjoxNTYxMDQzMzA1LCJzdWIiOiIifQ" + + ".jblwG_taIVf3IRiv200ivsc8q_IUj-M9QePKPAULfXdSZlY6H9n_XWtT6lw43k-J6QHfmnY4Yuh3eZq61KS7AT9yggM1VuolRCvYztSZ-MZHMIlvSE2KCc0wXa5gNQarjmDJloYduZuyLaKaRUUbO4osk1MuruODY_c2g2j16ce0Z8XVJ-7R8_J_Z8g0GdtFAfPO4bqpg9dj31MA8AKl3h-ru8NXcs3y1PkrYHpEGCgpcGcUQwLY7uiIrzjr0trCUbsLsv6iq2XTXnN_tTrfvL1R3yTB6gITvXZdsnU3r_UIDTzexTtlZWdntucJAGKX9HMA_jYEcTZ4ZhyEzETGpw"); + Assert.assertEquals(1, decoded.size()); + Assert.assertTrue(decoded.containsEntry("library/openjdk", "pull")); + Assert.assertFalse(decoded.containsEntry("library/openjdk", "push")); + Assert.assertFalse(decoded.containsEntry("randorepo", "push")); + } + + @Test + public void testDecode_nonToken() { + // something other than a JWT token + Multimap decoded = + Authorization.decodeTokenRepositoryGrants( + "VGhlc2UgYXJlbid0IHRoZSBKV1RzIHlvdSdyZSBsb29raW5nIGZvcgo"); + Assert.assertNull(decoded); + } + + @Test + public void testDecode_invalidToken_accessString() { + // a JWT with an "access" field that is not an array + Multimap decoded = + Authorization.decodeTokenRepositoryGrants( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOiJzdHJpbmcifQ.12ODBkkfh6J79qEejxwlD5AfOa9mjObPCzOnUL75NSQ"); + Assert.assertNull(decoded); + } + + @Test + public void testDecode_invalidToken_accessArray() { + // a JWT with an "access" field that is an array of non-claim objects + Multimap decoded = + Authorization.decodeTokenRepositoryGrants( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOlsic3RyaW5nIl19.gWZ9J4sO_w0hIVVxrfuuUC2lNhqkU3P0_z46xMCXfwU"); + Assert.assertNull(decoded); + } + + @Test + public void testDecode_invalidToken_actionsArray() { + // a JWT with an "access" field that is an action array of non-strings + Multimap decoded = + Authorization.decodeTokenRepositoryGrants( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6ImxpYnJhcnkvb3BlbmpkayIsImFjdGlvbnMiOlsxXX1dfQ.12HZGeFvthXw0PP9ZKdttJRh2qsRfFNTeZV3_lZiI10"); + Assert.assertNull(decoded); + } + + @Test + public void testDecode_invalidToken_randoJwt() { + // the JWT example token from jwt.io + Multimap decoded = + Authorization.decodeTokenRepositoryGrants( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" + + ".eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ" + + ".SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"); + Assert.assertNull(decoded); + } + + /** Basic credential should allow access to all. */ + @Test + public void testCanAccess_basicCredential() { + Authorization fixture = Authorization.fromBasicCredentials("foo", "bar"); + Assert.assertTrue(fixture.canAccess("random", "pull")); + } + + /** Basic token should allow access to all. */ + @Test + public void testCanAccess_basicToken() { + Authorization fixture = Authorization.fromBasicToken("gobbledygook"); + Assert.assertTrue(fixture.canAccess("random", "pull")); + } + + @Test + public void testCanAccess_bearer_withToken() { + // a synthetic token for accessing docker.io's openjdk with push and pull + Authorization authorization = + Authorization.fromBearerToken( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6ImxpYnJhcnkvb3BlbmpkayIsImFjdGlvbnMiOlsicHVsbCIsInB1c2giXX1dfQ.VEn96Ug4eseKHX3WwP3PlgR9P7Y6VuYmMm-YRUjngFg"); + Assert.assertNotNull(authorization); + Assert.assertTrue(authorization.canAccess("library/openjdk", "pull")); + Assert.assertTrue(authorization.canAccess("library/openjdk", "push")); + Assert.assertFalse(authorization.canAccess("library/openjdk", "other")); + Assert.assertFalse(authorization.canAccess("randorepo", "push")); + } + + @Test + public void testCanAccess_bearer_withNonToken() { + // non-Docker Bearer Tokens are assumed to allow access to all + Authorization authorization = + Authorization.fromBearerToken( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"); + Assert.assertNotNull(authorization); + Assert.assertTrue(authorization.canAccess("library/openjdk", "pull")); + Assert.assertTrue(authorization.canAccess("library/openjdk", "push")); + Assert.assertTrue(authorization.canAccess("randorepo", "push")); + } +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java index 7eae71f001..d1e96c1787 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/AuthenticationMethodRetrieverTest.java @@ -23,6 +23,7 @@ import com.google.cloud.tools.jib.http.Response; import java.net.MalformedURLException; import java.net.URL; +import java.util.Collections; import org.hamcrest.CoreMatchers; import org.junit.Assert; import org.junit.Test; @@ -149,6 +150,7 @@ public void testHandleHttpResponseException_pass() Assert.assertEquals( new URL("https://somerealm?service=someservice&scope=repository:someImageName:someScope"), - registryAuthenticator.getAuthenticationUrl(null, "someScope")); + registryAuthenticator.getAuthenticationUrl( + null, Collections.singletonMap("someImageName", "someScope"))); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java index d6481b8271..0f498051e6 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/RegistryAuthenticatorTest.java @@ -24,6 +24,7 @@ import java.net.URISyntaxException; import java.net.URL; import java.security.GeneralSecurityException; +import java.util.Collections; import org.hamcrest.CoreMatchers; import org.junit.Assert; import org.junit.Before; @@ -55,7 +56,8 @@ public void testFromAuthenticationMethod_bearer() "user-agent"); Assert.assertEquals( new URL("https://somerealm?service=someservice&scope=repository:someimage:scope"), - registryAuthenticator.getAuthenticationUrl(null, "scope")); + registryAuthenticator.getAuthenticationUrl( + null, Collections.singletonMap("someimage", "scope"))); registryAuthenticator = RegistryAuthenticator.fromAuthenticationMethod( @@ -64,14 +66,16 @@ public void testFromAuthenticationMethod_bearer() "user-agent"); Assert.assertEquals( new URL("https://somerealm?service=someservice&scope=repository:someimage:scope"), - registryAuthenticator.getAuthenticationUrl(null, "scope")); + registryAuthenticator.getAuthenticationUrl( + null, Collections.singletonMap("someimage", "scope"))); } @Test public void testAuthRequestParameters_basicAuth() { Assert.assertEquals( "service=someservice&scope=repository:someimage:scope", - registryAuthenticator.getAuthRequestParameters(null, "scope")); + registryAuthenticator.getAuthRequestParameters( + null, Collections.singletonMap("someimage", "scope"))); } @Test @@ -81,7 +85,8 @@ public void testAuthRequestParameters_oauth2() { "service=someservice&scope=repository:someimage:scope" + "&client_id=jib.da031fe481a93ac107a95a96462358f9" + "&grant_type=refresh_token&refresh_token=oauth2_access_token", - registryAuthenticator.getAuthRequestParameters(credential, "scope")); + registryAuthenticator.getAuthRequestParameters( + credential, Collections.singletonMap("someimage", "scope"))); } @Test @@ -105,7 +110,8 @@ public void isOAuth2Auth_oauth2() { public void getAuthenticationUrl_basicAuth() throws MalformedURLException { Assert.assertEquals( new URL("https://somerealm?service=someservice&scope=repository:someimage:scope"), - registryAuthenticator.getAuthenticationUrl(null, "scope")); + registryAuthenticator.getAuthenticationUrl( + null, Collections.singletonMap("someimage", "scope"))); } @Test @@ -113,7 +119,7 @@ public void istAuthenticationUrl_oauth2() throws MalformedURLException { Credential credential = Credential.from("", "oauth2_token"); Assert.assertEquals( new URL("https://somerealm"), - registryAuthenticator.getAuthenticationUrl(credential, "scope")); + registryAuthenticator.getAuthenticationUrl(credential, Collections.emptyMap())); } @Test @@ -176,7 +182,8 @@ public void testFromAuthenticationMethod_noService() Assert.assertEquals( new URL("https://somerealm?service=someserver&scope=repository:someimage:scope"), - registryAuthenticator.getAuthenticationUrl(null, "scope")); + registryAuthenticator.getAuthenticationUrl( + null, Collections.singletonMap("someimage", "scope"))); } @Test diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index 01134b0348..97332b5375 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -80,6 +80,12 @@ 7.0 compile + + com.auth0 + java-jwt + 3.8.1 + compile + From 8947afdae48379920844fca89059fe85e19b20a5 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Thu, 20 Jun 2019 13:23:49 -0400 Subject: [PATCH 03/12] move authorization check from PushBlobStep to RegistryClient; renames --- .../tools/jib/builder/steps/PushBlobStep.java | 9 ++---- .../jib/registry/RegistryAuthenticator.java | 28 ++++++++++--------- .../tools/jib/registry/RegistryClient.java | 5 ++++ .../RegistryEndpointRequestProperties.java | 2 +- 4 files changed, 23 insertions(+), 21 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java index c8d7e4dc35..aa97eaa8fa 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/builder/steps/PushBlobStep.java @@ -27,7 +27,6 @@ import com.google.cloud.tools.jib.builder.TimerEventDispatcher; import com.google.cloud.tools.jib.configuration.BuildConfiguration; import com.google.cloud.tools.jib.event.progress.ThrottledAccumulatingConsumer; -import com.google.cloud.tools.jib.http.Authorization; import com.google.cloud.tools.jib.registry.RegistryClient; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; @@ -83,11 +82,10 @@ public BlobDescriptor call() throws IOException, RegistryException, ExecutionExc buildConfiguration.getEventHandlers(), DESCRIPTION + blobDescriptor); ThrottledAccumulatingConsumer throttledProgressReporter = new ThrottledAccumulatingConsumer(progressEventDispatcher::dispatchProgress)) { - Authorization authorization = NonBlockingSteps.get(authenticatePushStep); RegistryClient registryClient = buildConfiguration .newTargetImageRegistryClientFactory() - .setAuthorization(authorization) + .setAuthorization(NonBlockingSteps.get(authenticatePushStep)) .newRegistryClient(); // check if the BLOB is available @@ -105,10 +103,7 @@ public BlobDescriptor call() throws IOException, RegistryException, ExecutionExc String baseRegistry = buildConfiguration.getBaseImageConfiguration().getImageRegistry(); String baseRepository = buildConfiguration.getBaseImageConfiguration().getImageRepository(); String targetRegistry = buildConfiguration.getTargetImageConfiguration().getImageRegistry(); - String sourceRepository = - targetRegistry.equals(baseRegistry) && authorization.canAccess(baseRepository, "pull") - ? baseRepository - : null; + String sourceRepository = targetRegistry.equals(baseRegistry) ? baseRepository : null; registryClient.pushBlob( blobDescriptor.getDigest(), blob, sourceRepository, throttledProgressReporter); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java index 9dadb82bc2..095d88994f 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java @@ -183,9 +183,9 @@ public Authorization authenticatePush(@Nullable Credential credential) } @VisibleForTesting - String getServiceScopeRequestParameters(Map scopes) { + String getServiceScopeRequestParameters(Map repositoryScopes) { StringBuilder parameters = new StringBuilder("service=").append(service); - for (Entry pair : scopes.entrySet()) { + for (Entry pair : repositoryScopes.entrySet()) { parameters .append("&scope=repository:") .append(pair.getKey()) @@ -196,16 +196,16 @@ String getServiceScopeRequestParameters(Map scopes) { } @VisibleForTesting - URL getAuthenticationUrl(@Nullable Credential credential, Map scopes) + URL getAuthenticationUrl(@Nullable Credential credential, Map repositoryScopes) throws MalformedURLException { return isOAuth2Auth(credential) ? new URL(realm) // Required parameters will be sent via POST . - : new URL(realm + "?" + getServiceScopeRequestParameters(scopes)); + : new URL(realm + "?" + getServiceScopeRequestParameters(repositoryScopes)); } @VisibleForTesting - String getAuthRequestParameters(@Nullable Credential credential, Map scopes) { - String serviceScope = getServiceScopeRequestParameters(scopes); + String getAuthRequestParameters(@Nullable Credential credential, Map repositoryScopes) { + String serviceScope = getServiceScopeRequestParameters(repositoryScopes); return isOAuth2Auth(credential) ? serviceScope // https://github.com/GoogleContainerTools/jib/pull/1545 @@ -251,23 +251,25 @@ private Authorization authenticate(@Nullable Credential credential, String scope // Unable to obtain authorization with source image: fallthrough and try without } } - Map scopes = + Map repositoryScopes = ImmutableMap.of(registryEndpointRequestProperties.getImageName(), scope); - Authorization auth = authenticate(credential, scopes); + Authorization auth = authenticate(credential, repositoryScopes); return auth; } - private Authorization authenticate(@Nullable Credential credential, Map scopes) + private Authorization authenticate( + @Nullable Credential credential, Map repositoryScopes) throws RegistryAuthenticationFailedException { try (Connection connection = - Connection.getConnectionFactory().apply(getAuthenticationUrl(credential, scopes))) { + Connection.getConnectionFactory() + .apply(getAuthenticationUrl(credential, repositoryScopes))) { Request.Builder requestBuilder = Request.builder() .setHttpTimeout(JibSystemProperties.getHttpTimeout()) .setUserAgent(userAgent); if (isOAuth2Auth(credential)) { - String parameters = getAuthRequestParameters(credential, scopes); + String parameters = getAuthRequestParameters(credential, repositoryScopes); requestBuilder.setBody( new BlobHttpContent(Blobs.from(parameters), MediaType.FORM_DATA.toString())); } else if (credential != null) { @@ -289,9 +291,9 @@ private Authorization authenticate(@Nullable Credential credential, Map writtenByteCountListener) throws IOException, RegistryException { + + if (authorization != null && !authorization.canAccess(sourceRepository, "pull")) { + // don't bother requesting a cross-repository blob-mount if we don't have access + sourceRepository = null; + } BlobPusher blobPusher = new BlobPusher(registryEndpointRequestProperties, blobDigest, blob, sourceRepository); diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointRequestProperties.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointRequestProperties.java index 15c15c5202..2e125ec847 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointRequestProperties.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryEndpointRequestProperties.java @@ -36,7 +36,7 @@ class RegistryEndpointRequestProperties { /** * @param serverUrl the server URL for the registry (for example, {@code gcr.io}) * @param imageName the image/repository name (also known as, namespace) - * @param sourceImageName additional source image on the registry + * @param sourceImageName additional source image to request pull permission from the registry */ RegistryEndpointRequestProperties( String serverUrl, String imageName, @Nullable String sourceImageName) { From b3d577cee52d38e2f7e09d002aac4462acc297c7 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Thu, 20 Jun 2019 14:48:58 -0400 Subject: [PATCH 04/12] add missing @Nullable --- .../java/com/google/cloud/tools/jib/http/Authorization.java | 4 +++- .../cloud/tools/jib/registry/RegistryAuthenticator.java | 3 ++- .../com/google/cloud/tools/jib/registry/RegistryClient.java | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java index 6bdc13844f..29818d57c4 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java @@ -58,6 +58,7 @@ public static Authorization fromBearerToken(String token) { * the token is not a Docker Registry Bearer Token */ @VisibleForTesting + @Nullable static Multimap decodeTokenRepositoryGrants(String token) { // Docker Registry Bearer Tokens are based on JWT. The payload looks like: // { @@ -128,7 +129,8 @@ public static Authorization fromBasicToken(String token) { private final String token; @Nullable private final Multimap repositoryGrants; - private Authorization(String scheme, String token, Multimap repositoryGrants) { + private Authorization( + String scheme, String token, @Nullable Multimap repositoryGrants) { this.scheme = scheme; this.token = token; this.repositoryGrants = repositoryGrants; diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java index 095d88994f..016c9d2685 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java @@ -204,7 +204,8 @@ URL getAuthenticationUrl(@Nullable Credential credential, Map re } @VisibleForTesting - String getAuthRequestParameters(@Nullable Credential credential, Map repositoryScopes) { + String getAuthRequestParameters( + @Nullable Credential credential, Map repositoryScopes) { String serviceScope = getServiceScopeRequestParameters(repositoryScopes); return isOAuth2Auth(credential) ? serviceScope diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java index 3b256d7484..0bf72a9067 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java @@ -301,7 +301,9 @@ public boolean pushBlob( Consumer writtenByteCountListener) throws IOException, RegistryException { - if (authorization != null && !authorization.canAccess(sourceRepository, "pull")) { + if (sourceRepository != null + && authorization != null + && !authorization.canAccess(sourceRepository, "pull")) { // don't bother requesting a cross-repository blob-mount if we don't have access sourceRepository = null; } From ce84808d1eb8722e55d5c0eed6b9f4312eadfdfd Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Thu, 20 Jun 2019 14:49:14 -0400 Subject: [PATCH 05/12] add java-jwt dependency --- jib-gradle-plugin/build.gradle | 1 + jib-plugins-common/build.gradle | 1 + 2 files changed, 2 insertions(+) diff --git a/jib-gradle-plugin/build.gradle b/jib-gradle-plugin/build.gradle index 64fda3e50d..b6ab8e8e0e 100644 --- a/jib-gradle-plugin/build.gradle +++ b/jib-gradle-plugin/build.gradle @@ -66,6 +66,7 @@ dependencies { compile 'com.google.guava:guava:27.0.1-jre' compile 'com.fasterxml.jackson.core:jackson-databind:2.9.9' compile 'org.ow2.asm:asm:7.0' + compile 'com.auth0:java-jwt:3.8.1' testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:2.23.4' diff --git a/jib-plugins-common/build.gradle b/jib-plugins-common/build.gradle index 5aa426dcd5..3ed47335ab 100644 --- a/jib-plugins-common/build.gradle +++ b/jib-plugins-common/build.gradle @@ -34,6 +34,7 @@ dependencies { compile 'com.google.guava:guava:27.0.1-jre' compile 'com.fasterxml.jackson.core:jackson-databind:2.9.9' compile 'org.ow2.asm:asm:7.0' + compile 'com.auth0:java-jwt:3.8.1' testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:2.23.4' From a4fc8228ba034dc51ccafa293ee6045ead761dc3 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Thu, 20 Jun 2019 15:28:33 -0400 Subject: [PATCH 06/12] Add CHANGELOG --- jib-gradle-plugin/CHANGELOG.md | 2 ++ jib-maven-plugin/CHANGELOG.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/jib-gradle-plugin/CHANGELOG.md b/jib-gradle-plugin/CHANGELOG.md index 11e4788230..9fe1da2973 100644 --- a/jib-gradle-plugin/CHANGELOG.md +++ b/jib-gradle-plugin/CHANGELOG.md @@ -11,6 +11,8 @@ All notable changes to this project will be documented in this file. ### Fixed +- Re-enabled cross-repository blob mounts ([#1793](https://github.com/GoogleContainerTools/jib/pull/1793)) + ## 1.3.0 ### Changed diff --git a/jib-maven-plugin/CHANGELOG.md b/jib-maven-plugin/CHANGELOG.md index 72c2f7badc..8b01eaeae1 100644 --- a/jib-maven-plugin/CHANGELOG.md +++ b/jib-maven-plugin/CHANGELOG.md @@ -11,6 +11,8 @@ All notable changes to this project will be documented in this file. ### Fixed +- Re-enabled cross-repository blob mounts ([#1793](https://github.com/GoogleContainerTools/jib/pull/1793)) + ## 1.3.0 ### Changed From 135e5cf83a1650324e2b15e7426f38ac3e7e000e Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Fri, 21 Jun 2019 17:05:16 -0400 Subject: [PATCH 07/12] Review notes: - be more explicit for JWT payload source - use JsonTemplateMapper rather than third-party library for parsing JWT - simplify RegistryClient factory use --- jib-core/build.gradle | 1 - .../cloud/tools/jib/http/Authorization.java | 93 +++++++++++-------- .../tools/jib/json/JsonTemplateMapper.java | 14 +++ .../tools/jib/registry/RegistryClient.java | 7 +- .../tools/jib/http/AuthorizationTest.java | 67 +++++++------ jib-gradle-plugin/build.gradle | 1 - jib-maven-plugin/pom.xml | 6 -- jib-plugins-common/build.gradle | 1 - 8 files changed, 108 insertions(+), 82 deletions(-) diff --git a/jib-core/build.gradle b/jib-core/build.gradle index c43a1f5e39..a7808dd240 100644 --- a/jib-core/build.gradle +++ b/jib-core/build.gradle @@ -40,7 +40,6 @@ dependencies { implementation 'com.google.guava:guava:27.0.1-jre' implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.9' implementation 'org.ow2.asm:asm:7.0' - implementation 'com.auth0:java-jwt:3.8.1' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:2.23.4' diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java index 29818d57c4..ef3bee64dd 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java @@ -16,20 +16,18 @@ package com.google.cloud.tools.jib.http; -import com.auth0.jwt.JWT; -import com.auth0.jwt.exceptions.JWTDecodeException; -import com.auth0.jwt.interfaces.DecodedJWT; -import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.google.api.client.util.Base64; +import com.google.cloud.tools.jib.json.JsonTemplate; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Strings; import com.google.common.collect.ImmutableSetMultimap; -import com.google.common.collect.Iterators; import com.google.common.collect.Multimap; -import com.google.common.collect.Streams; +import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; -import java.util.stream.Stream; import javax.annotation.Nullable; /** @@ -60,7 +58,16 @@ public static Authorization fromBearerToken(String token) { @VisibleForTesting @Nullable static Multimap decodeTokenRepositoryGrants(String token) { - // Docker Registry Bearer Tokens are based on JWT. The payload looks like: + // Docker Registry Bearer Tokens are based on JWT. A valid JWT is a set of 3 base64-encoded + // parts (header, payload, signature), collated with a ".". The header and payload are + // JSON objects. + String[] jwtParts = token.split("\\.", -1); + byte[] payloadData; + if (jwtParts.length != 3 || (payloadData = Base64.decodeBase64(jwtParts[1])) == null) { + return null; + } + + // The payload looks like: // { // "access":[{"type":"repository","name":"repository/name","actions":["pull"]}], // "aud":"registry.docker.io", @@ -71,41 +78,25 @@ static Multimap decodeTokenRepositoryGrants(String token) { // "nbf":999, // "sub":"e3ae001d-xxx" // } + // + TokenPayloadTemplate payload; try { - DecodedJWT jwt = JWT.decode(token); - // Make sure they look like valid access claims. - JsonNode[] accessClaims = jwt.getClaim("access").asArray(JsonNode.class); - if (accessClaims == null - || !Stream.of(accessClaims).allMatch(Authorization::isValidAccessClaim)) { - return null; - } - return Stream.of(accessClaims) - .filter(n -> "repository".equals(n.get("type").asText())) - .collect( - ImmutableSetMultimap.flatteningToImmutableSetMultimap( - n -> n.get("name").asText(), - n -> Streams.stream(n.get("actions").iterator()).map(JsonNode::asText))); - } catch (JWTDecodeException exception) { + payload = JsonTemplateMapper.readJson(payloadData, TokenPayloadTemplate.class); + } catch (IOException ex) { return null; } - } - - /** - * Check that the provided access object looks genuine: should be a JSON object with a non-empty - * string "type" field. - */ - private static boolean isValidAccessClaim(JsonNode n) { - if (!n.isObject() || Strings.isNullOrEmpty(n.get("type").asText())) { - return false; - } - if ("repository".equals(n.get("type").asText())) { - // repository should have a name and array of permitted actions - return !Strings.isNullOrEmpty(n.get("name").asText()) - && n.get("actions").isArray() - && Iterators.all(n.get("actions").iterator(), JsonNode::isTextual); + if (payload.access == null) { + return null; } - return true; + return payload + .access + .stream() + .filter(claim -> "repository".equals(claim.type)) + .collect( + ImmutableSetMultimap.flatteningToImmutableSetMultimap( + claim -> claim.name, claim -> claim.actions.stream())); } + /** * @param username the username * @param secret the secret @@ -178,4 +169,28 @@ public boolean canAccess(String repository, String access) { // if null then we assume that all repositories are granted return repositoryGrants == null || repositoryGrants.containsEntry(repository, access); } + + /** + * A simple class to represent a Docker Registry Bearer Token payload. + * + *

+   * {"access":[{"type": "repository","name": "library/openjdk","actions":["push","pull"]}]}
+   * 
+ */ + @JsonIgnoreProperties(ignoreUnknown = true) + private static class TokenPayloadTemplate implements JsonTemplate { + @Nullable private List access; + } + + /** + * Represents an access claim for a repository in a Docker Registry Bearer Token payload. + * + *
{"type": "repository","name": "library/openjdk","actions":["push","pull"]}
+ */ + @JsonIgnoreProperties(ignoreUnknown = true) + private static class AccessClaim implements JsonTemplate { + @Nullable private String type; + @Nullable private String name; + private List actions = new ArrayList<>(); + } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java b/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java index fe462059f6..45747d3f29 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/json/JsonTemplateMapper.java @@ -104,6 +104,20 @@ public static T readJson(String jsonString, Class te return objectMapper.readValue(jsonString, templateClass); } + /** + * Deserializes a JSON object from a JSON byte array. + * + * @param child type of {@link JsonTemplate} + * @param jsonBytes a JSON byte array + * @param templateClass the template to deserialize the string to + * @return the template filled with the values parsed from {@code jsonBytes} + * @throws IOException if an error occurred during parsing the JSON + */ + public static T readJson(byte[] jsonBytes, Class templateClass) + throws IOException { + return objectMapper.readValue(jsonBytes, templateClass); + } + /** * Deserializes a JSON object list from a JSON string. * diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java index 0bf72a9067..f87be26074 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java @@ -138,14 +138,11 @@ private String makeUserAgent() { * @return the new {@link Factory} */ public static Factory factory(EventHandlers eventHandlers, String serverUrl, String imageName) { - return factory(eventHandlers, serverUrl, imageName, null); + return new Factory(eventHandlers, new RegistryEndpointRequestProperties(serverUrl, imageName)); } public static Factory factory( - EventHandlers eventHandlers, - String serverUrl, - String imageName, - @Nullable String sourceImageName) { + EventHandlers eventHandlers, String serverUrl, String imageName, String sourceImageName) { return new Factory( eventHandlers, new RegistryEndpointRequestProperties(serverUrl, imageName, sourceImageName)); diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/AuthorizationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/AuthorizationTest.java index 8a94a1b65e..3210fd19ed 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/AuthorizationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/AuthorizationTest.java @@ -16,15 +16,22 @@ package com.google.cloud.tools.jib.http; +import com.google.api.client.util.Base64; import com.google.common.collect.Multimap; +import java.nio.charset.StandardCharsets; import org.junit.Assert; +import org.junit.Ignore; import org.junit.Test; -/** Tests for {@link Authorization}. */ +/** + * Tests for {@link Authorization}. + * + *

JWTs were generated from jwt.io's JWT debugger with HS256. + */ public class AuthorizationTest { @Test public void testDecode_dockerToken() { - // a genuine token for accessing docker.io's openjdk + // a genuine token from accessing docker.io's openjdk Multimap decoded = Authorization.decodeTokenRepositoryGrants( "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsIng1YyI6WyJNSUlDK2pDQ0FwK2dBd0lCQWdJQkFEQUtCZ2dxaGtqT1BRUURBakJHTVVRd1FnWURWUVFERXpzeVYwNVpPbFZMUzFJNlJFMUVVanBTU1U5Rk9reEhOa0U2UTFWWVZEcE5SbFZNT2tZelNFVTZOVkF5VlRwTFNqTkdPa05CTmxrNlNrbEVVVEFlRncweE9UQXhNVEl3TURJeU5EVmFGdzB5TURBeE1USXdNREl5TkRWYU1FWXhSREJDQmdOVkJBTVRPMUpMTkZNNlMwRkxVVHBEV0RWRk9rRTJSMVE2VTBwTVR6cFFNbEpMT2tOWlZVUTZTMEpEU0RwWFNVeE1Pa3hUU2xrNldscFFVVHBaVWxsRU1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcjY2bXkveXpHN21VUzF3eFQ3dFplS2pqRzcvNnBwZFNMY3JCcko5VytwcndzMGtIUDVwUHRkMUpkcFdEWU1OZWdqQXhpUWtRUUNvd25IUnN2ODVUalBUdE5wUkdKVTRkeHJkeXBvWGc4TVhYUEUzL2lRbHhPS2VNU0prNlRKbG5wNGFtWVBHQlhuQXRoQzJtTlR5ak1zdFh2ZmNWN3VFYWpRcnlOVUcyUVdXQ1k1Ujl0a2k5ZG54Z3dCSEF6bG8wTzJCczFmcm5JbmJxaCtic3ZSZ1FxU3BrMWhxYnhSU3AyRlNrL2tBL1gyeUFxZzJQSUJxWFFMaTVQQ3krWERYZElJczV6VG9ZbWJUK0pmbnZaMzRLcG5mSkpNalpIRW4xUVJtQldOZXJZcVdtNVhkQVhUMUJrQU9aditMNFVwSTk3NFZFZ2ppY1JINVdBeWV4b1BFclRRSURBUUFCbzRHeU1JR3ZNQTRHQTFVZER3RUIvd1FFQXdJSGdEQVBCZ05WSFNVRUNEQUdCZ1JWSFNVQU1FUUdBMVVkRGdROUJEdFNTelJUT2t0QlMxRTZRMWcxUlRwQk5rZFVPbE5LVEU4NlVESlNTenBEV1ZWRU9rdENRMGc2VjBsTVREcE1VMHBaT2xwYVVGRTZXVkpaUkRCR0JnTlZIU01FUHpBOWdEc3lWMDVaT2xWTFMxSTZSRTFFVWpwU1NVOUZPa3hITmtFNlExVllWRHBOUmxWTU9rWXpTRVU2TlZBeVZUcExTak5HT2tOQk5sazZTa2xFVVRBS0JnZ3Foa2pPUFFRREFnTkpBREJHQWlFQXFOSXEwMFdZTmM5Z2tDZGdSUzRSWUhtNTRZcDBTa05Rd2lyMm5hSWtGd3dDSVFEMjlYdUl5TmpTa1cvWmpQaFlWWFB6QW9TNFVkRXNvUUhyUVZHMDd1N3ZsUT09Il19" @@ -38,48 +45,48 @@ public void testDecode_dockerToken() { @Test public void testDecode_nonToken() { - // something other than a JWT token - Multimap decoded = - Authorization.decodeTokenRepositoryGrants( - "VGhlc2UgYXJlbid0IHRoZSBKV1RzIHlvdSdyZSBsb29raW5nIGZvcgo"); + String base64Text = + Base64.encodeBase64String( + "something other than a JWT token".getBytes(StandardCharsets.UTF_8)); + Multimap decoded = Authorization.decodeTokenRepositoryGrants(base64Text); Assert.assertNull(decoded); } @Test public void testDecode_invalidToken_accessString() { - // a JWT with an "access" field that is not an array - Multimap decoded = - Authorization.decodeTokenRepositoryGrants( - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOiJzdHJpbmcifQ.12ODBkkfh6J79qEejxwlD5AfOa9mjObPCzOnUL75NSQ"); + // a JWT with an "access" field that is not an array: {"access": "string"} + String jwt = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOiJzdHJpbmcifQ.12ODBkkfh6J79qEejxwlD5AfOa9mjObPCzOnUL75NSQ"; + Multimap decoded = Authorization.decodeTokenRepositoryGrants(jwt); Assert.assertNull(decoded); } @Test public void testDecode_invalidToken_accessArray() { - // a JWT with an "access" field that is an array of non-claim objects - Multimap decoded = - Authorization.decodeTokenRepositoryGrants( - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOlsic3RyaW5nIl19.gWZ9J4sO_w0hIVVxrfuuUC2lNhqkU3P0_z46xMCXfwU"); + // a JWT with an "access" field that is an array of non-claim objects: {"access":["string"]} + String jwt = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOlsic3RyaW5nIl19.gWZ9J4sO_w0hIVVxrfuuUC2lNhqkU3P0_z46xMCXfwU"; + Multimap decoded = Authorization.decodeTokenRepositoryGrants(jwt); Assert.assertNull(decoded); } @Test + @Ignore("Annotate AccessClaim.actions to disallow coercion of integers to strings") public void testDecode_invalidToken_actionsArray() { - // a JWT with an "access" field that is an action array of non-strings - Multimap decoded = - Authorization.decodeTokenRepositoryGrants( - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6ImxpYnJhcnkvb3BlbmpkayIsImFjdGlvbnMiOlsxXX1dfQ.12HZGeFvthXw0PP9ZKdttJRh2qsRfFNTeZV3_lZiI10"); + // a JWT with an "access" field that is an action array of non-strings: + // {"access":[{"type": "repository","name": "library/openjdk","actions":[1]}]} + String jwt = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6ImxpYnJhcnkvb3BlbmpkayIsImFjdGlvbnMiOlsxXX1dfQ.12HZGeFvthXw0PP9ZKdttJRh2qsRfFNTeZV3_lZiI10"; + Multimap decoded = Authorization.decodeTokenRepositoryGrants(jwt); Assert.assertNull(decoded); } @Test public void testDecode_invalidToken_randoJwt() { // the JWT example token from jwt.io - Multimap decoded = - Authorization.decodeTokenRepositoryGrants( - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" - + ".eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ" - + ".SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"); + String jwt = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; + Multimap decoded = Authorization.decodeTokenRepositoryGrants(jwt); Assert.assertNull(decoded); } @@ -100,9 +107,10 @@ public void testCanAccess_basicToken() { @Test public void testCanAccess_bearer_withToken() { // a synthetic token for accessing docker.io's openjdk with push and pull - Authorization authorization = - Authorization.fromBearerToken( - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6ImxpYnJhcnkvb3BlbmpkayIsImFjdGlvbnMiOlsicHVsbCIsInB1c2giXX1dfQ.VEn96Ug4eseKHX3WwP3PlgR9P7Y6VuYmMm-YRUjngFg"); + // {"access":[{"type":"repository","name":"library/openjdk","actions":["pull","push"]}]} + String token = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6ImxpYnJhcnkvb3BlbmpkayIsImFjdGlvbnMiOlsicHVsbCIsInB1c2giXX1dfQ.VEn96Ug4eseKHX3WwP3PlgR9P7Y6VuYmMm-YRUjngFg"; + Authorization authorization = Authorization.fromBearerToken(token); Assert.assertNotNull(authorization); Assert.assertTrue(authorization.canAccess("library/openjdk", "pull")); Assert.assertTrue(authorization.canAccess("library/openjdk", "push")); @@ -113,9 +121,10 @@ public void testCanAccess_bearer_withToken() { @Test public void testCanAccess_bearer_withNonToken() { // non-Docker Bearer Tokens are assumed to allow access to all - Authorization authorization = - Authorization.fromBearerToken( - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"); + // the JWT example token from jwt.io + String jwt = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; + Authorization authorization = Authorization.fromBearerToken(jwt); Assert.assertNotNull(authorization); Assert.assertTrue(authorization.canAccess("library/openjdk", "pull")); Assert.assertTrue(authorization.canAccess("library/openjdk", "push")); diff --git a/jib-gradle-plugin/build.gradle b/jib-gradle-plugin/build.gradle index b6ab8e8e0e..64fda3e50d 100644 --- a/jib-gradle-plugin/build.gradle +++ b/jib-gradle-plugin/build.gradle @@ -66,7 +66,6 @@ dependencies { compile 'com.google.guava:guava:27.0.1-jre' compile 'com.fasterxml.jackson.core:jackson-databind:2.9.9' compile 'org.ow2.asm:asm:7.0' - compile 'com.auth0:java-jwt:3.8.1' testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:2.23.4' diff --git a/jib-maven-plugin/pom.xml b/jib-maven-plugin/pom.xml index 97332b5375..01134b0348 100644 --- a/jib-maven-plugin/pom.xml +++ b/jib-maven-plugin/pom.xml @@ -80,12 +80,6 @@ 7.0 compile - - com.auth0 - java-jwt - 3.8.1 - compile - diff --git a/jib-plugins-common/build.gradle b/jib-plugins-common/build.gradle index 3ed47335ab..5aa426dcd5 100644 --- a/jib-plugins-common/build.gradle +++ b/jib-plugins-common/build.gradle @@ -34,7 +34,6 @@ dependencies { compile 'com.google.guava:guava:27.0.1-jre' compile 'com.fasterxml.jackson.core:jackson-databind:2.9.9' compile 'org.ow2.asm:asm:7.0' - compile 'com.auth0:java-jwt:3.8.1' testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:2.23.4' From 63ea371a2fb383c2ff776bdcc9a679d7936f5558 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Fri, 21 Jun 2019 22:16:43 -0400 Subject: [PATCH 08/12] review comments --- .../cloud/tools/jib/http/Authorization.java | 75 ++++++++++--------- .../jib/registry/RegistryAuthenticator.java | 2 +- .../tools/jib/http/AuthorizationTest.java | 16 ++-- 3 files changed, 53 insertions(+), 40 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java index ef3bee64dd..e8287b447d 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java @@ -25,9 +25,9 @@ import com.google.common.collect.Multimap; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.stream.Stream; import javax.annotation.Nullable; /** @@ -39,6 +39,25 @@ */ public class Authorization { + /** + * @param username the username + * @param secret the secret + * @return an {@link Authorization} with a {@code Basic} credentials + */ + public static Authorization fromBasicCredentials(String username, String secret) { + String credentials = username + ":" + secret; + String token = Base64.encodeBase64String(credentials.getBytes(StandardCharsets.UTF_8)); + return new Authorization("Basic", token, null); + } + + /** + * @param token the token + * @return an {@link Authorization} with a base64-encoded {@code username:password} string + */ + public static Authorization fromBasicToken(String token) { + return new Authorization("Basic", token, null); + } + /** * @param token the token * @return an {@link Authorization} with a {@code Bearer} token @@ -52,8 +71,8 @@ public static Authorization fromBearerToken(String token) { * Token to list the granted repositories with their levels of access. * * @param token a Docker Registry Bearer Token - * @return a mapping of repository to granted access scopes, or {@code null} if there is no JWT - * the token is not a Docker Registry Bearer Token + * @return a mapping of repository to granted access scopes, or {@code null} if the token is not a + * Docker Registry Bearer Token */ @VisibleForTesting @Nullable @@ -79,45 +98,33 @@ static Multimap decodeTokenRepositoryGrants(String token) { // "sub":"e3ae001d-xxx" // } // - TokenPayloadTemplate payload; try { - payload = JsonTemplateMapper.readJson(payloadData, TokenPayloadTemplate.class); + TokenPayloadTemplate payload = + JsonTemplateMapper.readJson(payloadData, TokenPayloadTemplate.class); + if (payload.access == null) { + return null; + } + return payload + .access + .stream() + .filter(claim -> "repository".equals(claim.type)) + .collect( + ImmutableSetMultimap.flatteningToImmutableSetMultimap( + claim -> claim.name, + claim -> claim.actions == null ? Stream.empty() : claim.actions.stream())); } catch (IOException ex) { return null; } - if (payload.access == null) { - return null; - } - return payload - .access - .stream() - .filter(claim -> "repository".equals(claim.type)) - .collect( - ImmutableSetMultimap.flatteningToImmutableSetMultimap( - claim -> claim.name, claim -> claim.actions.stream())); } - /** - * @param username the username - * @param secret the secret - * @return an {@link Authorization} with a {@code Basic} credentials - */ - public static Authorization fromBasicCredentials(String username, String secret) { - String credentials = username + ":" + secret; - String token = Base64.encodeBase64String(credentials.getBytes(StandardCharsets.UTF_8)); - return new Authorization("Basic", token, null); - } + private final String scheme; + private final String token; /** - * @param token the token - * @return an {@link Authorization} with a base64-encoded {@code username:password} string + * If token is a Docker Registry Bearer Token, then {@link #repositoryGrants} will contain a map + * of repository to the access grant information extracted from the token. Otherwise, it must be + * {@code null}, indicating that access to all repositories are permitted. */ - public static Authorization fromBasicToken(String token) { - return new Authorization("Basic", token, null); - } - - private final String scheme; - private final String token; @Nullable private final Multimap repositoryGrants; private Authorization( @@ -191,6 +198,6 @@ private static class TokenPayloadTemplate implements JsonTemplate { private static class AccessClaim implements JsonTemplate { @Nullable private String type; @Nullable private String name; - private List actions = new ArrayList<>(); + @Nullable private List actions; } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java index 016c9d2685..7820595a1c 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryAuthenticator.java @@ -234,7 +234,7 @@ boolean isOAuth2Auth(@Nullable Credential credential) { */ private Authorization authenticate(@Nullable Credential credential, String scope) throws RegistryAuthenticationFailedException { - // try authorizing againat both the main repository and the source repository too + // try authorizing against both the main repository and the source repository too // to enable cross-repository mounts on pushes if (registryEndpointRequestProperties.getSourceImageName() != null) { try { diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/AuthorizationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/http/AuthorizationTest.java index 3210fd19ed..4351187044 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/AuthorizationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/http/AuthorizationTest.java @@ -26,12 +26,18 @@ /** * Tests for {@link Authorization}. * - *

JWTs were generated from jwt.io's JWT debugger with HS256. + *

JWTs were generated from jwt.io's JWT Debugger. Set the + * algorithm to HS256, and paste the JSON shown as the Payload. */ public class AuthorizationTest { @Test public void testDecode_dockerToken() { - // a genuine token from accessing docker.io's openjdk + // A genuine token from accessing docker.io's openjdk: + // {"access":[{"type":"repository","name":"library/openjdk","actions":["pull"]}] + // Generated by + // $ cd examples/helloworld + // $ mvn package jib:dockerBuild -Djib.from.image=openjdk \ + // -Djava.util.logging.config.file= Multimap decoded = Authorization.decodeTokenRepositoryGrants( "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsIng1YyI6WyJNSUlDK2pDQ0FwK2dBd0lCQWdJQkFEQUtCZ2dxaGtqT1BRUURBakJHTVVRd1FnWURWUVFERXpzeVYwNVpPbFZMUzFJNlJFMUVVanBTU1U5Rk9reEhOa0U2UTFWWVZEcE5SbFZNT2tZelNFVTZOVkF5VlRwTFNqTkdPa05CTmxrNlNrbEVVVEFlRncweE9UQXhNVEl3TURJeU5EVmFGdzB5TURBeE1USXdNREl5TkRWYU1FWXhSREJDQmdOVkJBTVRPMUpMTkZNNlMwRkxVVHBEV0RWRk9rRTJSMVE2VTBwTVR6cFFNbEpMT2tOWlZVUTZTMEpEU0RwWFNVeE1Pa3hUU2xrNldscFFVVHBaVWxsRU1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcjY2bXkveXpHN21VUzF3eFQ3dFplS2pqRzcvNnBwZFNMY3JCcko5VytwcndzMGtIUDVwUHRkMUpkcFdEWU1OZWdqQXhpUWtRUUNvd25IUnN2ODVUalBUdE5wUkdKVTRkeHJkeXBvWGc4TVhYUEUzL2lRbHhPS2VNU0prNlRKbG5wNGFtWVBHQlhuQXRoQzJtTlR5ak1zdFh2ZmNWN3VFYWpRcnlOVUcyUVdXQ1k1Ujl0a2k5ZG54Z3dCSEF6bG8wTzJCczFmcm5JbmJxaCtic3ZSZ1FxU3BrMWhxYnhSU3AyRlNrL2tBL1gyeUFxZzJQSUJxWFFMaTVQQ3krWERYZElJczV6VG9ZbWJUK0pmbnZaMzRLcG5mSkpNalpIRW4xUVJtQldOZXJZcVdtNVhkQVhUMUJrQU9aditMNFVwSTk3NFZFZ2ppY1JINVdBeWV4b1BFclRRSURBUUFCbzRHeU1JR3ZNQTRHQTFVZER3RUIvd1FFQXdJSGdEQVBCZ05WSFNVRUNEQUdCZ1JWSFNVQU1FUUdBMVVkRGdROUJEdFNTelJUT2t0QlMxRTZRMWcxUlRwQk5rZFVPbE5LVEU4NlVESlNTenBEV1ZWRU9rdENRMGc2VjBsTVREcE1VMHBaT2xwYVVGRTZXVkpaUkRCR0JnTlZIU01FUHpBOWdEc3lWMDVaT2xWTFMxSTZSRTFFVWpwU1NVOUZPa3hITmtFNlExVllWRHBOUmxWTU9rWXpTRVU2TlZBeVZUcExTak5HT2tOQk5sazZTa2xFVVRBS0JnZ3Foa2pPUFFRREFnTkpBREJHQWlFQXFOSXEwMFdZTmM5Z2tDZGdSUzRSWUhtNTRZcDBTa05Rd2lyMm5hSWtGd3dDSVFEMjlYdUl5TmpTa1cvWmpQaFlWWFB6QW9TNFVkRXNvUUhyUVZHMDd1N3ZsUT09Il19" @@ -40,7 +46,7 @@ public void testDecode_dockerToken() { Assert.assertEquals(1, decoded.size()); Assert.assertTrue(decoded.containsEntry("library/openjdk", "pull")); Assert.assertFalse(decoded.containsEntry("library/openjdk", "push")); - Assert.assertFalse(decoded.containsEntry("randorepo", "push")); + Assert.assertFalse(decoded.containsEntry("randomrepo", "push")); } @Test @@ -115,7 +121,7 @@ public void testCanAccess_bearer_withToken() { Assert.assertTrue(authorization.canAccess("library/openjdk", "pull")); Assert.assertTrue(authorization.canAccess("library/openjdk", "push")); Assert.assertFalse(authorization.canAccess("library/openjdk", "other")); - Assert.assertFalse(authorization.canAccess("randorepo", "push")); + Assert.assertFalse(authorization.canAccess("randomrepo", "push")); } @Test @@ -128,6 +134,6 @@ public void testCanAccess_bearer_withNonToken() { Assert.assertNotNull(authorization); Assert.assertTrue(authorization.canAccess("library/openjdk", "pull")); Assert.assertTrue(authorization.canAccess("library/openjdk", "push")); - Assert.assertTrue(authorization.canAccess("randorepo", "push")); + Assert.assertTrue(authorization.canAccess("randomrepo", "push")); } } From f4e2d7ce64fa0d5cf4563c6f7338e4e27a3db531 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Mon, 24 Jun 2019 15:44:45 -0400 Subject: [PATCH 09/12] Move the docker-token related checks into RegistryClient --- .../cloud/tools/jib/http/Authorization.java | 114 +----------------- .../tools/jib/registry/RegistryClient.java | 114 +++++++++++++++++- .../DockerRegistryBearerTokenTest.java} | 46 +++---- 3 files changed, 138 insertions(+), 136 deletions(-) rename jib-core/src/test/java/com/google/cloud/tools/jib/{http/AuthorizationTest.java => registry/DockerRegistryBearerTokenTest.java} (80%) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java index e8287b447d..ff1f365862 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/http/Authorization.java @@ -16,19 +16,9 @@ package com.google.cloud.tools.jib.http; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.google.api.client.util.Base64; -import com.google.cloud.tools.jib.json.JsonTemplate; -import com.google.cloud.tools.jib.json.JsonTemplateMapper; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableSetMultimap; -import com.google.common.collect.Multimap; -import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.util.List; import java.util.Objects; -import java.util.stream.Stream; -import javax.annotation.Nullable; /** * Holds the credentials for an HTTP {@code Authorization} header. @@ -47,7 +37,7 @@ public class Authorization { public static Authorization fromBasicCredentials(String username, String secret) { String credentials = username + ":" + secret; String token = Base64.encodeBase64String(credentials.getBytes(StandardCharsets.UTF_8)); - return new Authorization("Basic", token, null); + return new Authorization("Basic", token); } /** @@ -55,7 +45,7 @@ public static Authorization fromBasicCredentials(String username, String secret) * @return an {@link Authorization} with a base64-encoded {@code username:password} string */ public static Authorization fromBasicToken(String token) { - return new Authorization("Basic", token, null); + return new Authorization("Basic", token); } /** @@ -63,75 +53,15 @@ public static Authorization fromBasicToken(String token) { * @return an {@link Authorization} with a {@code Bearer} token */ public static Authorization fromBearerToken(String token) { - return new Authorization("Bearer", token, decodeTokenRepositoryGrants(token)); - } - - /** - * Decode the Docker Registry v2 Bearer - * Token to list the granted repositories with their levels of access. - * - * @param token a Docker Registry Bearer Token - * @return a mapping of repository to granted access scopes, or {@code null} if the token is not a - * Docker Registry Bearer Token - */ - @VisibleForTesting - @Nullable - static Multimap decodeTokenRepositoryGrants(String token) { - // Docker Registry Bearer Tokens are based on JWT. A valid JWT is a set of 3 base64-encoded - // parts (header, payload, signature), collated with a ".". The header and payload are - // JSON objects. - String[] jwtParts = token.split("\\.", -1); - byte[] payloadData; - if (jwtParts.length != 3 || (payloadData = Base64.decodeBase64(jwtParts[1])) == null) { - return null; - } - - // The payload looks like: - // { - // "access":[{"type":"repository","name":"repository/name","actions":["pull"]}], - // "aud":"registry.docker.io", - // "iss":"auth.docker.io", - // "exp":999, - // "iat":999, - // "jti":"zzzz", - // "nbf":999, - // "sub":"e3ae001d-xxx" - // } - // - try { - TokenPayloadTemplate payload = - JsonTemplateMapper.readJson(payloadData, TokenPayloadTemplate.class); - if (payload.access == null) { - return null; - } - return payload - .access - .stream() - .filter(claim -> "repository".equals(claim.type)) - .collect( - ImmutableSetMultimap.flatteningToImmutableSetMultimap( - claim -> claim.name, - claim -> claim.actions == null ? Stream.empty() : claim.actions.stream())); - } catch (IOException ex) { - return null; - } + return new Authorization("Bearer", token); } private final String scheme; private final String token; - /** - * If token is a Docker Registry Bearer Token, then {@link #repositoryGrants} will contain a map - * of repository to the access grant information extracted from the token. Otherwise, it must be - * {@code null}, indicating that access to all repositories are permitted. - */ - @Nullable private final Multimap repositoryGrants; - - private Authorization( - String scheme, String token, @Nullable Multimap repositoryGrants) { + private Authorization(String scheme, String token) { this.scheme = scheme; this.token = token; - this.repositoryGrants = repositoryGrants; } public String getScheme() { @@ -164,40 +94,4 @@ public boolean equals(Object other) { public int hashCode() { return Objects.hash(scheme, token); } - - /** - * Check if this authorization allows accessing the specified repository. - * - * @param repository repository in question - * @param access the access scope ("push" or "pull") - * @return true if the repository was covered - */ - public boolean canAccess(String repository, String access) { - // if null then we assume that all repositories are granted - return repositoryGrants == null || repositoryGrants.containsEntry(repository, access); - } - - /** - * A simple class to represent a Docker Registry Bearer Token payload. - * - *

-   * {"access":[{"type": "repository","name": "library/openjdk","actions":["push","pull"]}]}
-   * 
- */ - @JsonIgnoreProperties(ignoreUnknown = true) - private static class TokenPayloadTemplate implements JsonTemplate { - @Nullable private List access; - } - - /** - * Represents an access claim for a repository in a Docker Registry Bearer Token payload. - * - *
{"type": "repository","name": "library/openjdk","actions":["push","pull"]}
- */ - @JsonIgnoreProperties(ignoreUnknown = true) - private static class AccessClaim implements JsonTemplate { - @Nullable private String type; - @Nullable private String name; - @Nullable private List actions; - } } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java index f87be26074..3c83a3cb65 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java @@ -16,6 +16,8 @@ package com.google.cloud.tools.jib.registry; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.google.api.client.util.Base64; import com.google.cloud.tools.jib.ProjectInfo; import com.google.cloud.tools.jib.api.DescriptorDigest; import com.google.cloud.tools.jib.api.RegistryException; @@ -30,12 +32,18 @@ import com.google.cloud.tools.jib.image.json.ManifestTemplate; import com.google.cloud.tools.jib.image.json.V21ManifestTemplate; import com.google.cloud.tools.jib.image.json.V22ManifestTemplate; +import com.google.cloud.tools.jib.json.JsonTemplate; +import com.google.cloud.tools.jib.json.JsonTemplateMapper; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.base.Verify; +import com.google.common.collect.ImmutableSetMultimap; +import com.google.common.collect.Multimap; import java.io.IOException; import java.net.URL; +import java.util.List; import java.util.function.Consumer; +import java.util.stream.Stream; import javax.annotation.Nullable; /** Interfaces with a registry. */ @@ -148,6 +156,85 @@ public static Factory factory( new RegistryEndpointRequestProperties(serverUrl, imageName, sourceImageName)); } + /** + * A simple class representing the payload of a Docker Registry v2 Bearer Token + * which lists the set of access claims granted. + * + *
+   * {"access":[{"type": "repository","name": "library/openjdk","actions":["push","pull"]}]}
+   * 
+ * + * @see AccessClaim + */ + @JsonIgnoreProperties(ignoreUnknown = true) + private static class TokenPayloadTemplate implements JsonTemplate { + @Nullable private List access; + } + + /** + * Represents an access claim for a repository in a Docker Registry Bearer Token payload. + * + *
{"type": "repository","name": "library/openjdk","actions":["push","pull"]}
+ */ + @JsonIgnoreProperties(ignoreUnknown = true) + private static class AccessClaim implements JsonTemplate { + @Nullable private String type; + @Nullable private String name; + @Nullable private List actions; + } + + /** + * Decode the Docker Registry v2 Bearer + * Token to list the granted repositories with their levels of access. + * + * @param token a Docker Registry Bearer Token + * @return a mapping of repository to granted access scopes, or {@code null} if the token is not a + * Docker Registry Bearer Token + */ + @VisibleForTesting + @Nullable + static Multimap decodeTokenRepositoryGrants(String token) { + // Docker Registry Bearer Tokens are based on JWT. A valid JWT is a set of 3 base64-encoded + // parts (header, payload, signature), collated with a ".". The header and payload are + // JSON objects. + String[] jwtParts = token.split("\\.", -1); + byte[] payloadData; + if (jwtParts.length != 3 || (payloadData = Base64.decodeBase64(jwtParts[1])) == null) { + return null; + } + + // The payload looks like: + // { + // "access":[{"type":"repository","name":"repository/name","actions":["pull"]}], + // "aud":"registry.docker.io", + // "iss":"auth.docker.io", + // "exp":999, + // "iat":999, + // "jti":"zzzz", + // "nbf":999, + // "sub":"e3ae001d-xxx" + // } + // + try { + TokenPayloadTemplate payload = + JsonTemplateMapper.readJson(payloadData, TokenPayloadTemplate.class); + if (payload.access == null) { + return null; + } + return payload + .access + .stream() + .filter(claim -> "repository".equals(claim.type)) + .collect( + ImmutableSetMultimap.flatteningToImmutableSetMultimap( + claim -> claim.name, + claim -> claim.actions == null ? Stream.empty() : claim.actions.stream())); + } catch (IOException ex) { + return null; + } + } + private final EventHandlers eventHandlers; @Nullable private final Authorization authorization; private final RegistryEndpointRequestProperties registryEndpointRequestProperties; @@ -298,9 +385,7 @@ public boolean pushBlob( Consumer writtenByteCountListener) throws IOException, RegistryException { - if (sourceRepository != null - && authorization != null - && !authorization.canAccess(sourceRepository, "pull")) { + if (sourceRepository != null && canMountBlobs(authorization, sourceRepository)) { // don't bother requesting a cross-repository blob-mount if we don't have access sourceRepository = null; } @@ -337,6 +422,29 @@ public boolean pushBlob( } } + /** + * Check if the authorization allows using the specified repository can be mounted by the remote + * registry as a source for blobs. More specifically, we can only check if the repository is not + * disallowed. + * + * @param repository repository in question + * @return {@code true} if the repository appears to be mountable + */ + @VisibleForTesting + static boolean canMountBlobs(@Nullable Authorization authorization, String repository) { + if (authorization == null || !"bearer".equalsIgnoreCase(authorization.getScheme())) { + // Authorization methods other than the Docker Container Registry Token don't provide + // information as to which repositories are accessible. The caller should attempt the mount + // and rely on the registry fallback as required by the spec. + // https://docs.docker.com/registry/spec/api/#pushing-an-image + return true; + } + // if null then does not appear to be a DCRT + Multimap repositoryGrants = + decodeTokenRepositoryGrants(authorization.getToken()); + return repositoryGrants == null || repositoryGrants.containsEntry(repository, "pull"); + } + /** @return the registry endpoint's API root, without the protocol */ @VisibleForTesting String getApiRouteBase() { diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/http/AuthorizationTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/DockerRegistryBearerTokenTest.java similarity index 80% rename from jib-core/src/test/java/com/google/cloud/tools/jib/http/AuthorizationTest.java rename to jib-core/src/test/java/com/google/cloud/tools/jib/registry/DockerRegistryBearerTokenTest.java index 4351187044..abdae7efca 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/http/AuthorizationTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/DockerRegistryBearerTokenTest.java @@ -14,9 +14,10 @@ * limitations under the License. */ -package com.google.cloud.tools.jib.http; +package com.google.cloud.tools.jib.registry; import com.google.api.client.util.Base64; +import com.google.cloud.tools.jib.http.Authorization; import com.google.common.collect.Multimap; import java.nio.charset.StandardCharsets; import org.junit.Assert; @@ -24,12 +25,12 @@ import org.junit.Test; /** - * Tests for {@link Authorization}. + * Tests for {@link RegistryClient} around handling of Docker Registry Bearer Tokens. * *

JWTs were generated from jwt.io's JWT Debugger. Set the * algorithm to HS256, and paste the JSON shown as the Payload. */ -public class AuthorizationTest { +public class DockerRegistryBearerTokenTest { @Test public void testDecode_dockerToken() { // A genuine token from accessing docker.io's openjdk: @@ -39,7 +40,7 @@ public void testDecode_dockerToken() { // $ mvn package jib:dockerBuild -Djib.from.image=openjdk \ // -Djava.util.logging.config.file= Multimap decoded = - Authorization.decodeTokenRepositoryGrants( + RegistryClient.decodeTokenRepositoryGrants( "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsIng1YyI6WyJNSUlDK2pDQ0FwK2dBd0lCQWdJQkFEQUtCZ2dxaGtqT1BRUURBakJHTVVRd1FnWURWUVFERXpzeVYwNVpPbFZMUzFJNlJFMUVVanBTU1U5Rk9reEhOa0U2UTFWWVZEcE5SbFZNT2tZelNFVTZOVkF5VlRwTFNqTkdPa05CTmxrNlNrbEVVVEFlRncweE9UQXhNVEl3TURJeU5EVmFGdzB5TURBeE1USXdNREl5TkRWYU1FWXhSREJDQmdOVkJBTVRPMUpMTkZNNlMwRkxVVHBEV0RWRk9rRTJSMVE2VTBwTVR6cFFNbEpMT2tOWlZVUTZTMEpEU0RwWFNVeE1Pa3hUU2xrNldscFFVVHBaVWxsRU1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcjY2bXkveXpHN21VUzF3eFQ3dFplS2pqRzcvNnBwZFNMY3JCcko5VytwcndzMGtIUDVwUHRkMUpkcFdEWU1OZWdqQXhpUWtRUUNvd25IUnN2ODVUalBUdE5wUkdKVTRkeHJkeXBvWGc4TVhYUEUzL2lRbHhPS2VNU0prNlRKbG5wNGFtWVBHQlhuQXRoQzJtTlR5ak1zdFh2ZmNWN3VFYWpRcnlOVUcyUVdXQ1k1Ujl0a2k5ZG54Z3dCSEF6bG8wTzJCczFmcm5JbmJxaCtic3ZSZ1FxU3BrMWhxYnhSU3AyRlNrL2tBL1gyeUFxZzJQSUJxWFFMaTVQQ3krWERYZElJczV6VG9ZbWJUK0pmbnZaMzRLcG5mSkpNalpIRW4xUVJtQldOZXJZcVdtNVhkQVhUMUJrQU9aditMNFVwSTk3NFZFZ2ppY1JINVdBeWV4b1BFclRRSURBUUFCbzRHeU1JR3ZNQTRHQTFVZER3RUIvd1FFQXdJSGdEQVBCZ05WSFNVRUNEQUdCZ1JWSFNVQU1FUUdBMVVkRGdROUJEdFNTelJUT2t0QlMxRTZRMWcxUlRwQk5rZFVPbE5LVEU4NlVESlNTenBEV1ZWRU9rdENRMGc2VjBsTVREcE1VMHBaT2xwYVVGRTZXVkpaUkRCR0JnTlZIU01FUHpBOWdEc3lWMDVaT2xWTFMxSTZSRTFFVWpwU1NVOUZPa3hITmtFNlExVllWRHBOUmxWTU9rWXpTRVU2TlZBeVZUcExTak5HT2tOQk5sazZTa2xFVVRBS0JnZ3Foa2pPUFFRREFnTkpBREJHQWlFQXFOSXEwMFdZTmM5Z2tDZGdSUzRSWUhtNTRZcDBTa05Rd2lyMm5hSWtGd3dDSVFEMjlYdUl5TmpTa1cvWmpQaFlWWFB6QW9TNFVkRXNvUUhyUVZHMDd1N3ZsUT09Il19" + ".eyJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6ImxpYnJhcnkvb3BlbmpkayIsImFjdGlvbnMiOlsicHVsbCJdfV0sImF1ZCI6InJlZ2lzdHJ5LmRvY2tlci5pbyIsImV4cCI6MTU2MTA0MzkwNSwiaWF0IjoxNTYxMDQzNjA1LCJpc3MiOiJhdXRoLmRvY2tlci5pbyIsImp0aSI6Ikc5bWpiOE9GeU5STFlpY3ZUMFZxIiwibmJmIjoxNTYxMDQzMzA1LCJzdWIiOiIifQ" + ".jblwG_taIVf3IRiv200ivsc8q_IUj-M9QePKPAULfXdSZlY6H9n_XWtT6lw43k-J6QHfmnY4Yuh3eZq61KS7AT9yggM1VuolRCvYztSZ-MZHMIlvSE2KCc0wXa5gNQarjmDJloYduZuyLaKaRUUbO4osk1MuruODY_c2g2j16ce0Z8XVJ-7R8_J_Z8g0GdtFAfPO4bqpg9dj31MA8AKl3h-ru8NXcs3y1PkrYHpEGCgpcGcUQwLY7uiIrzjr0trCUbsLsv6iq2XTXnN_tTrfvL1R3yTB6gITvXZdsnU3r_UIDTzexTtlZWdntucJAGKX9HMA_jYEcTZ4ZhyEzETGpw"); @@ -54,7 +55,7 @@ public void testDecode_nonToken() { String base64Text = Base64.encodeBase64String( "something other than a JWT token".getBytes(StandardCharsets.UTF_8)); - Multimap decoded = Authorization.decodeTokenRepositoryGrants(base64Text); + Multimap decoded = RegistryClient.decodeTokenRepositoryGrants(base64Text); Assert.assertNull(decoded); } @@ -63,7 +64,7 @@ public void testDecode_invalidToken_accessString() { // a JWT with an "access" field that is not an array: {"access": "string"} String jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOiJzdHJpbmcifQ.12ODBkkfh6J79qEejxwlD5AfOa9mjObPCzOnUL75NSQ"; - Multimap decoded = Authorization.decodeTokenRepositoryGrants(jwt); + Multimap decoded = RegistryClient.decodeTokenRepositoryGrants(jwt); Assert.assertNull(decoded); } @@ -72,7 +73,7 @@ public void testDecode_invalidToken_accessArray() { // a JWT with an "access" field that is an array of non-claim objects: {"access":["string"]} String jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOlsic3RyaW5nIl19.gWZ9J4sO_w0hIVVxrfuuUC2lNhqkU3P0_z46xMCXfwU"; - Multimap decoded = Authorization.decodeTokenRepositoryGrants(jwt); + Multimap decoded = RegistryClient.decodeTokenRepositoryGrants(jwt); Assert.assertNull(decoded); } @@ -83,7 +84,7 @@ public void testDecode_invalidToken_actionsArray() { // {"access":[{"type": "repository","name": "library/openjdk","actions":[1]}]} String jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6ImxpYnJhcnkvb3BlbmpkayIsImFjdGlvbnMiOlsxXX1dfQ.12HZGeFvthXw0PP9ZKdttJRh2qsRfFNTeZV3_lZiI10"; - Multimap decoded = Authorization.decodeTokenRepositoryGrants(jwt); + Multimap decoded = RegistryClient.decodeTokenRepositoryGrants(jwt); Assert.assertNull(decoded); } @@ -92,48 +93,47 @@ public void testDecode_invalidToken_randoJwt() { // the JWT example token from jwt.io String jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; - Multimap decoded = Authorization.decodeTokenRepositoryGrants(jwt); + Multimap decoded = RegistryClient.decodeTokenRepositoryGrants(jwt); Assert.assertNull(decoded); } /** Basic credential should allow access to all. */ @Test - public void testCanAccess_basicCredential() { + public void testCanMountBlobs_basicCredential() { Authorization fixture = Authorization.fromBasicCredentials("foo", "bar"); - Assert.assertTrue(fixture.canAccess("random", "pull")); + Assert.assertTrue(RegistryClient.canMountBlobs(fixture, "random")); } /** Basic token should allow access to all. */ @Test - public void testCanAccess_basicToken() { + public void testCanMountBlobs_basicToken() { + // basic tokens are assumed to allow all repositories to be mounted Authorization fixture = Authorization.fromBasicToken("gobbledygook"); - Assert.assertTrue(fixture.canAccess("random", "pull")); + Assert.assertTrue(RegistryClient.canMountBlobs(fixture, "library/openjdk")); + Assert.assertTrue(RegistryClient.canMountBlobs(fixture, "randomrepo")); } @Test - public void testCanAccess_bearer_withToken() { + public void testCanMountBlobs_bearer_withToken() { // a synthetic token for accessing docker.io's openjdk with push and pull // {"access":[{"type":"repository","name":"library/openjdk","actions":["pull","push"]}]} String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6ImxpYnJhcnkvb3BlbmpkayIsImFjdGlvbnMiOlsicHVsbCIsInB1c2giXX1dfQ.VEn96Ug4eseKHX3WwP3PlgR9P7Y6VuYmMm-YRUjngFg"; Authorization authorization = Authorization.fromBearerToken(token); Assert.assertNotNull(authorization); - Assert.assertTrue(authorization.canAccess("library/openjdk", "pull")); - Assert.assertTrue(authorization.canAccess("library/openjdk", "push")); - Assert.assertFalse(authorization.canAccess("library/openjdk", "other")); - Assert.assertFalse(authorization.canAccess("randomrepo", "push")); + Assert.assertTrue(RegistryClient.canMountBlobs(authorization, "library/openjdk")); + Assert.assertFalse(RegistryClient.canMountBlobs(authorization, "randomrepo")); } @Test - public void testCanAccess_bearer_withNonToken() { - // non-Docker Bearer Tokens are assumed to allow access to all + public void testCanMountBlobs_bearer_withNonToken() { + // non-Docker Registry Bearer Tokens are assumed to allow access to all // the JWT example token from jwt.io String jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; Authorization authorization = Authorization.fromBearerToken(jwt); Assert.assertNotNull(authorization); - Assert.assertTrue(authorization.canAccess("library/openjdk", "pull")); - Assert.assertTrue(authorization.canAccess("library/openjdk", "push")); - Assert.assertTrue(authorization.canAccess("randomrepo", "push")); + Assert.assertTrue(RegistryClient.canMountBlobs(authorization, "library/openjdk")); + Assert.assertTrue(RegistryClient.canMountBlobs(authorization, "randomrepo")); } } From c80449500932c8a6ae8eef47793570936ba876fa Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Mon, 24 Jun 2019 16:17:55 -0400 Subject: [PATCH 10/12] canMountBlobs -> canAttemptBlobMount, fix embarrassing inverted test --- .../tools/jib/registry/RegistryClient.java | 4 ++-- .../DockerRegistryBearerTokenTest.java | 22 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java index 3c83a3cb65..57aa3ccddb 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java @@ -385,7 +385,7 @@ public boolean pushBlob( Consumer writtenByteCountListener) throws IOException, RegistryException { - if (sourceRepository != null && canMountBlobs(authorization, sourceRepository)) { + if (sourceRepository != null && !canAttemptBlobMount(authorization, sourceRepository)) { // don't bother requesting a cross-repository blob-mount if we don't have access sourceRepository = null; } @@ -431,7 +431,7 @@ public boolean pushBlob( * @return {@code true} if the repository appears to be mountable */ @VisibleForTesting - static boolean canMountBlobs(@Nullable Authorization authorization, String repository) { + static boolean canAttemptBlobMount(@Nullable Authorization authorization, String repository) { if (authorization == null || !"bearer".equalsIgnoreCase(authorization.getScheme())) { // Authorization methods other than the Docker Container Registry Token don't provide // information as to which repositories are accessible. The caller should attempt the mount diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/DockerRegistryBearerTokenTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/DockerRegistryBearerTokenTest.java index abdae7efca..451d8ba1da 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/registry/DockerRegistryBearerTokenTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/registry/DockerRegistryBearerTokenTest.java @@ -99,41 +99,41 @@ public void testDecode_invalidToken_randoJwt() { /** Basic credential should allow access to all. */ @Test - public void testCanMountBlobs_basicCredential() { + public void testCanAttemptBlobMount_basicCredential() { Authorization fixture = Authorization.fromBasicCredentials("foo", "bar"); - Assert.assertTrue(RegistryClient.canMountBlobs(fixture, "random")); + Assert.assertTrue(RegistryClient.canAttemptBlobMount(fixture, "random")); } /** Basic token should allow access to all. */ @Test - public void testCanMountBlobs_basicToken() { + public void testCanAttemptBlobMount_basicToken() { // basic tokens are assumed to allow all repositories to be mounted Authorization fixture = Authorization.fromBasicToken("gobbledygook"); - Assert.assertTrue(RegistryClient.canMountBlobs(fixture, "library/openjdk")); - Assert.assertTrue(RegistryClient.canMountBlobs(fixture, "randomrepo")); + Assert.assertTrue(RegistryClient.canAttemptBlobMount(fixture, "library/openjdk")); + Assert.assertTrue(RegistryClient.canAttemptBlobMount(fixture, "randomrepo")); } @Test - public void testCanMountBlobs_bearer_withToken() { + public void testCanAttemptBlobMount_bearer_withToken() { // a synthetic token for accessing docker.io's openjdk with push and pull // {"access":[{"type":"repository","name":"library/openjdk","actions":["pull","push"]}]} String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6ImxpYnJhcnkvb3BlbmpkayIsImFjdGlvbnMiOlsicHVsbCIsInB1c2giXX1dfQ.VEn96Ug4eseKHX3WwP3PlgR9P7Y6VuYmMm-YRUjngFg"; Authorization authorization = Authorization.fromBearerToken(token); Assert.assertNotNull(authorization); - Assert.assertTrue(RegistryClient.canMountBlobs(authorization, "library/openjdk")); - Assert.assertFalse(RegistryClient.canMountBlobs(authorization, "randomrepo")); + Assert.assertTrue(RegistryClient.canAttemptBlobMount(authorization, "library/openjdk")); + Assert.assertFalse(RegistryClient.canAttemptBlobMount(authorization, "randomrepo")); } @Test - public void testCanMountBlobs_bearer_withNonToken() { + public void testCanAttemptBlobMount_bearer_withNonToken() { // non-Docker Registry Bearer Tokens are assumed to allow access to all // the JWT example token from jwt.io String jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; Authorization authorization = Authorization.fromBearerToken(jwt); Assert.assertNotNull(authorization); - Assert.assertTrue(RegistryClient.canMountBlobs(authorization, "library/openjdk")); - Assert.assertTrue(RegistryClient.canMountBlobs(authorization, "randomrepo")); + Assert.assertTrue(RegistryClient.canAttemptBlobMount(authorization, "library/openjdk")); + Assert.assertTrue(RegistryClient.canAttemptBlobMount(authorization, "randomrepo")); } } From 5c71abf4aacf676caebac54e9d655cd3a9a62687 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Mon, 24 Jun 2019 22:20:30 -0400 Subject: [PATCH 11/12] Add ability to disable blob-mounts with `jib.blobMounts` flag --- .../tools/jib/global/JibSystemProperties.java | 16 +++++++++- .../tools/jib/registry/RegistryClient.java | 4 ++- .../jib/global/JibSystemPropertiesTest.java | 30 +++++++++++++++++++ 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java b/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java index 9f8829558e..bf01d1d399 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java @@ -25,11 +25,13 @@ public class JibSystemProperties { @VisibleForTesting public static final String HTTP_TIMEOUT = "jib.httpTimeout"; + @VisibleForTesting static final String CROSS_REPOSITORY_BLOB_MOUNTS = "jib.blobMounts"; + @VisibleForTesting public static final String SEND_CREDENTIALS_OVER_HTTP = "sendCredentialsOverHttp"; private static final String SERIALIZE = "jibSerialize"; - + private static final String DISABLE_USER_AGENT = "_JIB_DISABLE_USER_AGENT"; /** @@ -46,6 +48,18 @@ public static int getHttpTimeout() { return Integer.getInteger(HTTP_TIMEOUT); } + /** + * Gets whether or not to use cross-repository blob mounts when uploading image layers + * ({@code mount/from}). This is defined by the {@code jib.blobMounts} system property. + * + * @return {@code true} if {@code mount/from} should be used, {@code false} if not, defaulting to + * {@code true} + */ + public static boolean useCrossRepositoryBlobMounts() { + return System.getProperty(CROSS_REPOSITORY_BLOB_MOUNTS) == null + || Boolean.getBoolean(CROSS_REPOSITORY_BLOB_MOUNTS); + } + /** * Gets whether or not to serialize Jib's execution. This is defined by the {@code jibSerialize} * system property. diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java index 57aa3ccddb..4dfe8dd52b 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/registry/RegistryClient.java @@ -385,7 +385,9 @@ public boolean pushBlob( Consumer writtenByteCountListener) throws IOException, RegistryException { - if (sourceRepository != null && !canAttemptBlobMount(authorization, sourceRepository)) { + if (sourceRepository != null + && !(JibSystemProperties.useCrossRepositoryBlobMounts() + && canAttemptBlobMount(authorization, sourceRepository))) { // don't bother requesting a cross-repository blob-mount if we don't have access sourceRepository = null; } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/global/JibSystemPropertiesTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/global/JibSystemPropertiesTest.java index f055a3c789..4fca87bef0 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/global/JibSystemPropertiesTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/global/JibSystemPropertiesTest.java @@ -45,6 +45,7 @@ public void tearDown() { if (httpsProxyPortSaved != null) { System.setProperty("https.proxyPort", httpsProxyPortSaved); } + System.clearProperty(JibSystemProperties.CROSS_REPOSITORY_BLOB_MOUNTS); } @Test @@ -152,4 +153,33 @@ public void testCheckHttpProxyPortProperty_stringValue() { Assert.assertEquals("https.proxyPort must be an integer: some string", ex.getMessage()); } } + + @Test + public void testUseBlobMountsPropertyName() { + Assert.assertEquals("jib.blobMounts", JibSystemProperties.CROSS_REPOSITORY_BLOB_MOUNTS); + } + + @Test + public void testUseBlobMounts_undefined() { + System.clearProperty(JibSystemProperties.CROSS_REPOSITORY_BLOB_MOUNTS); + Assert.assertTrue(JibSystemProperties.useCrossRepositoryBlobMounts()); + } + + @Test + public void testUseBlobMounts_true() { + System.setProperty(JibSystemProperties.CROSS_REPOSITORY_BLOB_MOUNTS, "true"); + Assert.assertTrue(JibSystemProperties.useCrossRepositoryBlobMounts()); + } + + @Test + public void testUseBlobMounts_false() { + System.setProperty(JibSystemProperties.CROSS_REPOSITORY_BLOB_MOUNTS, "false"); + Assert.assertFalse(JibSystemProperties.useCrossRepositoryBlobMounts()); + } + + @Test + public void testUseBlobMounts_other() { + System.setProperty(JibSystemProperties.CROSS_REPOSITORY_BLOB_MOUNTS, "nonbool"); + Assert.assertFalse(JibSystemProperties.useCrossRepositoryBlobMounts()); + } } From e2c372dc185547f930df6753ea3508671a6d0536 Mon Sep 17 00:00:00 2001 From: Brian de Alwis Date: Mon, 24 Jun 2019 22:29:07 -0400 Subject: [PATCH 12/12] whitespace --- .../com/google/cloud/tools/jib/global/JibSystemProperties.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java b/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java index bf01d1d399..cf78836645 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/global/JibSystemProperties.java @@ -31,7 +31,7 @@ public class JibSystemProperties { public static final String SEND_CREDENTIALS_OVER_HTTP = "sendCredentialsOverHttp"; private static final String SERIALIZE = "jibSerialize"; - + private static final String DISABLE_USER_AGENT = "_JIB_DISABLE_USER_AGENT"; /**