Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Re-enable cross-repository blob mounts #1793

Merged
merged 12 commits into from
Jun 25, 2019
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -47,91 +37,31 @@ 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);
}

/**
* @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);
return new Authorization("Basic", token);
}

/**
* @param token the 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 <a href="https://docs.docker.com/registry/spec/auth/jwt/">Docker Registry v2 Bearer
* Token</a> 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<String, String> 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.<AccessClaim, String, String>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<String, String> repositoryGrants;

private Authorization(
String scheme, String token, @Nullable Multimap<String, String> repositoryGrants) {
private Authorization(String scheme, String token) {
this.scheme = scheme;
this.token = token;
this.repositoryGrants = repositoryGrants;
}

public String getScheme() {
Expand Down Expand Up @@ -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.
*
* <pre>
* {"access":[{"type": "repository","name": "library/openjdk","actions":["push","pull"]}]}
* </pre>
*/
@JsonIgnoreProperties(ignoreUnknown = true)
private static class TokenPayloadTemplate implements JsonTemplate {
@Nullable private List<AccessClaim> access;
}

/**
* Represents an access claim for a repository in a Docker Registry Bearer Token payload.
*
* <pre>{"type": "repository","name": "library/openjdk","actions":["push","pull"]}</pre>
*/
@JsonIgnoreProperties(ignoreUnknown = true)
private static class AccessClaim implements JsonTemplate {
@Nullable private String type;
@Nullable private String name;
@Nullable private List<String> actions;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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. */
Expand Down Expand Up @@ -148,6 +156,85 @@ public static Factory factory(
new RegistryEndpointRequestProperties(serverUrl, imageName, sourceImageName));
}

/**
* A simple class representing the payload of a <a
* href="https://docs.docker.com/registry/spec/auth/jwt/">Docker Registry v2 Bearer Token</a>
* which lists the set of access claims granted.
*
* <pre>
* {"access":[{"type": "repository","name": "library/openjdk","actions":["push","pull"]}]}
* </pre>
*
* @see AccessClaim
*/
@JsonIgnoreProperties(ignoreUnknown = true)
private static class TokenPayloadTemplate implements JsonTemplate {
@Nullable private List<AccessClaim> access;
}

/**
* Represents an access claim for a repository in a Docker Registry Bearer Token payload.
*
* <pre>{"type": "repository","name": "library/openjdk","actions":["push","pull"]}</pre>
*/
@JsonIgnoreProperties(ignoreUnknown = true)
private static class AccessClaim implements JsonTemplate {
@Nullable private String type;
@Nullable private String name;
@Nullable private List<String> actions;
}

/**
* Decode the <a href="https://docs.docker.com/registry/spec/auth/jwt/">Docker Registry v2 Bearer
* Token</a> 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<String, String> 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.<AccessClaim, String, String>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;
Expand Down Expand Up @@ -298,9 +385,7 @@ public boolean pushBlob(
Consumer<Long> writtenByteCountListener)
throws IOException, RegistryException {

if (sourceRepository != null
&& authorization != null
&& !authorization.canAccess(sourceRepository, "pull")) {
if (sourceRepository != null && canMountBlobs(authorization, sourceRepository)) {
briandealwis marked this conversation as resolved.
Show resolved Hide resolved
// don't bother requesting a cross-repository blob-mount if we don't have access
sourceRepository = null;
}
Expand Down Expand Up @@ -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) {
briandealwis marked this conversation as resolved.
Show resolved Hide resolved
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<String, String> repositoryGrants =
decodeTokenRepositoryGrants(authorization.getToken());
return repositoryGrants == null || repositoryGrants.containsEntry(repository, "pull");
}

/** @return the registry endpoint's API root, without the protocol */
@VisibleForTesting
String getApiRouteBase() {
Expand Down
Loading