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

Optimize authentication for base image #2789

Merged
merged 4 commits into from
Sep 30, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions jib-core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ All notable changes to this project will be documented in this file.
- Fixed `NullPointerException` to return a helpful message when a server does not provide any message in certain error cases (400 Bad Request, 404 Not Found, and 405 Method Not Allowed). ([#2532](https://github.com/GoogleContainerTools/jib/issues/2532))
- Now supports sending client certificate (for example, via the `javax.net.ssl.keyStore` and `javax.net.ssl.keyStorePassword` system properties) and thus enabling mutual TLS authentication. ([#2585](https://github.com/GoogleContainerTools/jib/issues/2585), [#2226](https://github.com/GoogleContainerTools/jib/issues/2226))
- Fixed `NullPointerException` during input validation (in Java 9+) when configuring Jib parameters using certain immutable collections (such as `List.of()`). ([#2702](https://github.com/GoogleContainerTools/jib/issues/2702))
- Fixed authentication failure with Azure Container Registry when using ["tokens"](https://docs.microsoft.com/en-us/azure/container-registry/container-registry-repository-scoped-permissions). ([#2784](https://github.com/GoogleContainerTools/jib/issues/2784))
- Improved authentication flow for base image registry. ([#2134](https://github.com/GoogleContainerTools/jib/issues/2134))

## 0.15.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,10 @@ public ImagesAndRegistryClient call()
progressEventDispatcherFactory.create("pulling base image manifest", 2);
TimerEventDispatcher ignored1 = new TimerEventDispatcher(eventHandlers, DESCRIPTION)) {

// First, try with no credentials.
// First, try with no credentials. This works with public GCR images (but not Docker Hub).
// TODO: investigate if we should just pass credentials up front. However, this involves
// some risk. https://github.com/GoogleContainerTools/jib/pull/2200#discussion_r359069026
// contains some related discussions.
RegistryClient noAuthRegistryClient =
buildContext.newBaseImageRegistryClientFactory().newRegistryClient();
try {
Expand All @@ -149,40 +152,43 @@ public ImagesAndRegistryClient call()
LogEvent.lifecycle(
"The base image requires auth. Trying again for " + imageReference + "..."));

Credential registryCredential =
Credential credential =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems a little strange that we have so much auth code in PullBaseImage step and not somewhere else like RegistryAuthenticator?

Copy link
Member Author

@chanseokoh chanseokoh Sep 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(First of all, I don't know the initial intention of RegistryAuthenticator, but in reality it has always been "BearerAuthenticator" that's only about bearer auth.)

As discussed offline, we could have introduced an overarching RegistryClient.doRightAuth() that perhaps does

if (!doPull/PushBearerAuth()) {
 ... 
 configureBasicAuth();
}

I expect this flow should work for both pull and push auth with any registries, and this is exactly the flow we do for target registry auth. However, we were taking a different flow for base image registries (as can be seen in the PR), and what is one of the reasons we don't have doRightAuth().

Given this and that RegistryClient is a low-level API, it also seemed reasonable to have separate nobs for configuring basic auth and completing bearer auth. Another factor to this is the difference between configuring basic auth and completing the whole bearer auth leg: configuring basic auth means that you'll just send the credentials going forward no matter what, while completing bearer auth means interacting with a registry server and an auth server with two round-trips to get another form of credentials to send going forward.

RegistryCredentialRetriever.getBaseImageCredential(buildContext).orElse(null);

RegistryClient registryClient =
buildContext
.newBaseImageRegistryClientFactory()
.setCredential(registryCredential)
.setCredential(credential)
.newRegistryClient();

try {
// TODO: refactor the code (https://github.com/GoogleContainerTools/jib/pull/2202)
if (registryCredential == null || registryCredential.isOAuth2RefreshToken()) {
throw ex;
}

eventHandlers.dispatch(LogEvent.debug("Trying basic auth for " + imageReference + "..."));
registryClient.configureBasicAuth();
String wwwAuthenticate = ex.getHttpResponseException().getHeaders().getAuthenticate();
if (wwwAuthenticate != null) {
eventHandlers.dispatch(
LogEvent.debug("WWW-Authenticate for " + imageReference + ": " + wwwAuthenticate));
registryClient.authPullByWwwAuthenticate(wwwAuthenticate);
return new ImagesAndRegistryClient(
pullBaseImages(registryClient, progressEventDispatcher), registryClient);

} catch (RegistryUnauthorizedException registryUnauthorizedException) {
// The registry requires us to authenticate using the Docker Token Authentication.
// See https://docs.docker.com/registry/spec/auth/token
eventHandlers.dispatch(
LogEvent.debug("Trying bearer auth for " + imageReference + "..."));
if (registryClient.doPullBearerAuth()) {
return new ImagesAndRegistryClient(
pullBaseImages(registryClient, progressEventDispatcher), registryClient);
} else {
// Not getting WWW-Authenticate is unexpected in practice, and we may just blame the
// server and fail. However, to keep some old behavior, try a few things as a last resort.
// TODO: consider removing this fallback branch.
if (credential != null && !credential.isOAuth2RefreshToken()) {
eventHandlers.dispatch(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we actually do LogEvent.warn here with some guidance on reporting this, so that we may identify misbehaving servers by user reports?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think this is a good idea to identify registries that are acting strange. But up to you.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I missed this comment. I thought about that, and I agree logging a warning will be very valuable to us. But I think a warning should be something actionable and fixable on the user side, so that's my hesitation. I've seen some people regard a warning seriously (probably because of a 0-warning policy auto-enforced by some system). Gathering information is really useful to us, but I feel we should be careful for dual-purposing a warning.

LogEvent.debug("Trying basic auth as fallback for " + imageReference + "..."));
registryClient.configureBasicAuth();
try {
return new ImagesAndRegistryClient(
pullBaseImages(registryClient, progressEventDispatcher), registryClient);
} catch (RegistryUnauthorizedException ignored) {
// Fall back to try bearer auth.
}
}

eventHandlers.dispatch(
LogEvent.error(
"The registry asked for basic authentication, but the registry had refused basic "
+ "authentication previously"));
throw registryUnauthorizedException;
LogEvent.debug("Trying bearer auth as fallback for " + imageReference + "..."));
registryClient.doPullBearerAuth();
return new ImagesAndRegistryClient(
pullBaseImages(registryClient, progressEventDispatcher), registryClient);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -325,15 +325,26 @@ private boolean doBearerAuth(boolean readOnlyBearerAuth) throws IOException, Reg
return false; // server returned "WWW-Authenticate: Basic ..."
}

initialBearerAuthenticator.set(authenticator.get());
doBearerAuth(readOnlyBearerAuth, authenticator.get());
return true;
}

private void doBearerAuth(boolean readOnlyBearerAuth, RegistryAuthenticator authenticator)
throws RegistryException {
initialBearerAuthenticator.set(authenticator);
if (readOnlyBearerAuth) {
authorization.set(authenticator.get().authenticatePull(credential));
authorization.set(authenticator.authenticatePull(credential));
} else {
authorization.set(authenticator.get().authenticatePush(credential));
authorization.set(authenticator.authenticatePush(credential));
}
this.readOnlyBearerAuth = readOnlyBearerAuth;
eventHandlers.dispatch(LogEvent.debug("bearer auth succeeded for " + image));
return true;

eventHandlers.dispatch(
LogEvent.debug(
"bearer auth succeeded for "
+ registryEndpointRequestProperties.getServerUrl()
+ "/"
+ registryEndpointRequestProperties.getImageName()));
}

private Authorization refreshBearerAuth(@Nullable String wwwAuthenticate)
Expand Down Expand Up @@ -366,6 +377,27 @@ private Authorization refreshBearerAuth(@Nullable String wwwAuthenticate)
return Verify.verifyNotNull(initialBearerAuthenticator.get()).authenticatePush(credential);
}

/**
* Configure basic authentication or attempts bearer authentication for pulling based on the
* specified authentication method in a server response.
*
* @param wwwAuthenticate {@code WWW-Authenticate} HTTP header value from a server response
* specifying a required authentication method
* @throws RegistryException if communicating with the endpoint fails
* @throws RegistryAuthenticationFailedException if authentication fails
* @throws RegistryCredentialsNotSentException if authentication failed and credentials were not
*/
public void authPullByWwwAuthenticate(String wwwAuthenticate) throws RegistryException {
Optional<RegistryAuthenticator> authenticator =
RegistryAuthenticator.fromAuthenticationMethod(
wwwAuthenticate, registryEndpointRequestProperties, getUserAgent(), httpClient);
if (authenticator.isPresent()) {
doBearerAuth(true, authenticator.get());
} else if (credential != null && !credential.isOAuth2RefreshToken()) {
configureBasicAuth();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this flow a little confusing. Should we just have multiple types of credentials instead of checking the state of the Credential everywhere?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like defining subclasses of Credentials?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I don't know, I've haven't done much in this section of the code base, so it's all a little strange.

}
}

/**
* Check if a manifest referred to by {@code imageQualifier} (tag or digest) exists on the
* registry.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,93 @@ public void testConfigureBasicAuth()
registry.getInputRead(), CoreMatchers.containsString("Authorization: Basic dXNlcjpwYXNz"));
}

@Test
public void testAuthPullByWwwAuthenticate_bearerAuth()
throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException,
RegistryException {
String tokenResponse = "HTTP/1.1 200 OK\nContent-Length: 26\n\n{\"token\":\"awesome-token!\"}";
authServer = new TestWebServer(false, Arrays.asList(tokenResponse), 1);

String blobResponse = "HTTP/1.1 200 OK\nContent-Length: 5678\n\n";
registry = new TestWebServer(false, Arrays.asList(blobResponse), 1);

RegistryClient registryClient = createRegistryClient(Credential.from("user", "pass"));
registryClient.authPullByWwwAuthenticate("Bearer realm=\"" + authServer.getEndpoint() + "\"");

Optional<BlobDescriptor> digestAndSize = registryClient.checkBlob(digest);
Assert.assertEquals(5678, digestAndSize.get().getSize());

Mockito.verify(eventHandlers).dispatch(logContains("bearer auth succeeded"));
}

@Test
public void testAuthPullByWwwAuthenticate_basicAuth()
throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException,
RegistryException {
String blobResponse = "HTTP/1.1 200 OK\nContent-Length: 5678\n\n";
registry = new TestWebServer(false, Arrays.asList(blobResponse), 1);

RegistryClient registryClient = createRegistryClient(Credential.from("user", "pass"));
registryClient.authPullByWwwAuthenticate("Basic foo");

Optional<BlobDescriptor> digestAndSize = registryClient.checkBlob(digest);
Assert.assertEquals(5678, digestAndSize.get().getSize());

MatcherAssert.assertThat(
registry.getInputRead(), CoreMatchers.containsString("Authorization: Basic dXNlcjpwYXNz"));
}

@Test
public void testAuthPullByWwwAuthenticate_basicAuthRequestedButNullCredential()
throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException,
RegistryException {
String blobResponse = "HTTP/1.1 200 OK\nContent-Length: 5678\n\n";
registry = new TestWebServer(false, Arrays.asList(blobResponse), 1);

RegistryClient registryClient = createRegistryClient(null);
registryClient.authPullByWwwAuthenticate("Basic foo");

Optional<BlobDescriptor> digestAndSize = registryClient.checkBlob(digest);
Assert.assertEquals(5678, digestAndSize.get().getSize());

MatcherAssert.assertThat(
registry.getInputRead(), CoreMatchers.not(CoreMatchers.containsString("Authorization:")));
}

@Test
public void testAuthPullByWwwAuthenticate_basicAuthRequestedButOAuth2Credential()
throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException,
RegistryException {
String blobResponse = "HTTP/1.1 200 OK\nContent-Length: 5678\n\n";
registry = new TestWebServer(false, Arrays.asList(blobResponse), 1);

Credential credential = Credential.from(Credential.OAUTH2_TOKEN_USER_NAME, "pass");
Assert.assertTrue(credential.isOAuth2RefreshToken());
RegistryClient registryClient = createRegistryClient(credential);
registryClient.authPullByWwwAuthenticate("Basic foo");

Optional<BlobDescriptor> digestAndSize = registryClient.checkBlob(digest);
Assert.assertEquals(5678, digestAndSize.get().getSize());

MatcherAssert.assertThat(
registry.getInputRead(), CoreMatchers.not(CoreMatchers.containsString("Authorization:")));
}

@Test
public void testAuthPullByWwwAuthenticate_invalidAuthMethod() {
RegistryClient registryClient =
RegistryClient.factory(eventHandlers, "server", "foo/bar", null).newRegistryClient();
try {
registryClient.authPullByWwwAuthenticate("invalid WWW-Authenticate");
Assert.fail();
} catch (RegistryException ex) {
Assert.assertEquals(
"Failed to authenticate with registry server/foo/bar because: 'Bearer' was not found in "
+ "the 'WWW-Authenticate' header, tried to parse: invalid WWW-Authenticate",
ex.getMessage());
}
}

@Test
public void testPullManifest()
throws IOException, InterruptedException, GeneralSecurityException, URISyntaxException,
Expand Down
2 changes: 2 additions & 0 deletions jib-gradle-plugin/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ All notable changes to this project will be documented in this file.

- Fixed `NullPointerException` during input validation (in Java 9+) when configuring Jib parameters using certain immutable collections (such as `List.of()`). ([#2702](https://github.com/GoogleContainerTools/jib/issues/2702))
- Fixed an issue that configuring `jib.from.platforms` was always additive to the default `amd64/linux` platform. ([#2783](https://github.com/GoogleContainerTools/jib/issues/2783))
- Fixed authentication failure with Azure Container Registry when using ["tokens"](https://docs.microsoft.com/en-us/azure/container-registry/container-registry-repository-scoped-permissions). ([#2784](https://github.com/GoogleContainerTools/jib/issues/2784))
- Improved authentication flow for base image registry. ([#2134](https://github.com/GoogleContainerTools/jib/issues/2134))
- Throw `IllegalArgumentException` with an error message instead of throwing a `NullPointerException` when `jib.to.tags` is set to a collection containing a `null` value. ([#2760](https://github.com/GoogleContainerTools/jib/issues/2760))

## 2.5.0
Expand Down
3 changes: 3 additions & 0 deletions jib-maven-plugin/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ All notable changes to this project will be documented in this file.

### Fixed

- Fixed authentication failure with Azure Container Registry when using ["tokens"](https://docs.microsoft.com/en-us/azure/container-registry/container-registry-repository-scoped-permissions). ([#2784](https://github.com/GoogleContainerTools/jib/issues/2784))
- Improved authentication flow for base image registry. ([#2134](https://github.com/GoogleContainerTools/jib/issues/2134))

## 2.5.2

### Fixed
Expand Down