-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
WIP: Docker authentiation using credential store/helpers #647
Changes from all commits
9fa3761
e6b95c4
c747e0a
89c6446
6a6b5fc
0b73459
abd398d
a83b3b0
8aaf528
0ace7ab
7957261
fcd5053
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -46,3 +46,5 @@ node_modules/ | |
|
||
.gradle/ | ||
build/ | ||
out/ | ||
*.class |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
package org.testcontainers.utility; | ||
|
||
import com.fasterxml.jackson.databind.JsonNode; | ||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import com.github.dockerjava.api.model.AuthConfig; | ||
import com.google.common.annotations.VisibleForTesting; | ||
import org.slf4j.Logger; | ||
import org.zeroturnaround.exec.ProcessExecutor; | ||
|
||
import java.io.ByteArrayInputStream; | ||
import java.io.File; | ||
import java.util.concurrent.TimeUnit; | ||
|
||
import static org.slf4j.LoggerFactory.getLogger; | ||
|
||
/** | ||
* Utility to look up registry authentication information for an image. | ||
*/ | ||
public class RegistryAuthLocator { | ||
|
||
private static final Logger log = getLogger(RegistryAuthLocator.class); | ||
|
||
private final AuthConfig defaultAuthConfig; | ||
private final File configFile; | ||
private final String commandPathPrefix; | ||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); | ||
|
||
@VisibleForTesting | ||
RegistryAuthLocator(AuthConfig defaultAuthConfig, File configFile, String commandPathPrefix) { | ||
this.defaultAuthConfig = defaultAuthConfig; | ||
this.configFile = configFile; | ||
this.commandPathPrefix = commandPathPrefix; | ||
} | ||
|
||
/** | ||
* @param defaultAuthConfig an AuthConfig object that should be returned if there is no overriding authentication | ||
* available for images that are looked up | ||
*/ | ||
public RegistryAuthLocator(AuthConfig defaultAuthConfig) { | ||
this.defaultAuthConfig = defaultAuthConfig; | ||
final String dockerConfigLocation = System.getenv().getOrDefault("DOCKER_CONFIG", | ||
System.getProperty("user.home") + "/.docker"); | ||
this.configFile = new File(dockerConfigLocation + "/config.json"); | ||
this.commandPathPrefix = ""; | ||
} | ||
|
||
/** | ||
* Looks up an AuthConfig for a given image name. | ||
* | ||
* @param dockerImageName image name to be looked up (potentially including a registry URL part) | ||
* @return an AuthConfig that is applicable to this specific image OR the defaultAuthConfig that has been set for | ||
* this {@link RegistryAuthLocator}. | ||
*/ | ||
public AuthConfig lookupAuthConfig(DockerImageName dockerImageName) { | ||
log.debug("Looking up auth config for image: {}", dockerImageName); | ||
|
||
log.debug("RegistryAuthLocator has configFile: {} ({}) and commandPathPrefix: {}", | ||
configFile, | ||
configFile.exists() ? "exists" : "does not exist", | ||
commandPathPrefix); | ||
|
||
try { | ||
final JsonNode config = OBJECT_MAPPER.readTree(configFile); | ||
|
||
final String reposName = dockerImageName.getRegistry(); | ||
final JsonNode auths = config.at("/auths/" + reposName); | ||
|
||
if (!auths.isMissingNode() && auths.size() == 0) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've tested this with Docker-for-Mac, works perfectly fine if changed to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Checked Jackson source code, should be safe to leave |
||
// auths/<registry> is an empty dict - use a credential helper | ||
return authConfigUsingCredentialsStoreOrHelper(reposName, config); | ||
} | ||
// otherwise, defaultAuthConfig should already contain any credentials available | ||
} catch (Exception e) { | ||
log.error("Failure when attempting to lookup auth config (dockerImageName: {}, configFile: {}. " + | ||
"Falling back to docker-java default behaviour", | ||
dockerImageName, | ||
configFile, | ||
e); | ||
} | ||
return defaultAuthConfig; | ||
} | ||
|
||
private AuthConfig authConfigUsingCredentialsStoreOrHelper(String hostName, JsonNode config) throws Exception { | ||
|
||
final JsonNode credsStoreName = config.at("/credsStore"); | ||
final JsonNode credHelper = config.at("/credHelpers/" + hostName); | ||
|
||
if (!credHelper.isMissingNode()) { | ||
return runCredentialProvider(hostName, credHelper.asText()); | ||
} else if (!credsStoreName.isMissingNode()) { | ||
return runCredentialProvider(hostName, credsStoreName.asText()); | ||
} else { | ||
throw new IllegalStateException("Unsupported Docker config auths settings!"); | ||
} | ||
} | ||
|
||
private AuthConfig runCredentialProvider(String hostName, String credHelper) throws Exception { | ||
final String credentialHelperName = commandPathPrefix + "docker-credential-" + credHelper; | ||
String data; | ||
|
||
log.debug("Executing docker credential helper: {} to locate auth config for: {}", | ||
credentialHelperName, hostName); | ||
|
||
try { | ||
data = new ProcessExecutor() | ||
.command(credentialHelperName, "get") | ||
.redirectInput(new ByteArrayInputStream(hostName.getBytes())) | ||
.readOutput(true) | ||
.exitValueNormal() | ||
.timeout(30, TimeUnit.SECONDS) | ||
.execute() | ||
.outputUTF8() | ||
.trim(); | ||
} catch (Exception e) { | ||
log.error("Failure running docker credential helper ({})", credentialHelperName); | ||
throw e; | ||
} | ||
|
||
final JsonNode helperResponse = OBJECT_MAPPER.readTree(data); | ||
log.debug("Credential helper provided auth config for: {}", hostName); | ||
|
||
return new AuthConfig() | ||
.withRegistryAddress(helperResponse.at("/ServerURL").asText()) | ||
.withUsername(helperResponse.at("/Username").asText()) | ||
.withPassword(helperResponse.at("/Secret").asText()); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
package org.testcontainers.utility; | ||
|
||
import com.github.dockerjava.api.model.AuthConfig; | ||
import com.google.common.io.Resources; | ||
import org.apache.commons.lang.SystemUtils; | ||
import org.jetbrains.annotations.NotNull; | ||
import org.junit.Assume; | ||
import org.junit.BeforeClass; | ||
import org.junit.Test; | ||
|
||
import java.io.File; | ||
import java.net.URISyntaxException; | ||
|
||
import static org.rnorth.visibleassertions.VisibleAssertions.assertEquals; | ||
import static org.rnorth.visibleassertions.VisibleAssertions.assertNull; | ||
|
||
public class RegistryAuthLocatorTest { | ||
|
||
@BeforeClass | ||
public static void nonWindowsTest() throws Exception { | ||
Assume.assumeFalse(SystemUtils.IS_OS_WINDOWS); | ||
} | ||
|
||
@Test | ||
public void lookupAuthConfigWithoutCredentials() throws URISyntaxException { | ||
final RegistryAuthLocator authLocator = createTestAuthLocator("config-with-store.json"); | ||
|
||
final AuthConfig authConfig = authLocator.lookupAuthConfig(new DockerImageName("unauthenticated.registry.org/org/repo")); | ||
|
||
assertEquals("Default docker registry URL is set on auth config", "https://index.docker.io/v1/", authConfig.getRegistryAddress()); | ||
assertNull("No username is set", authConfig.getUsername()); | ||
assertNull("No password is set", authConfig.getPassword()); | ||
} | ||
|
||
@Test | ||
public void lookupAuthConfigUsingStore() throws URISyntaxException { | ||
final RegistryAuthLocator authLocator = createTestAuthLocator("config-with-store.json"); | ||
|
||
final AuthConfig authConfig = authLocator.lookupAuthConfig(new DockerImageName("registry.example.com/org/repo")); | ||
|
||
assertEquals("Correct server URL is obtained from a credential store", "url", authConfig.getRegistryAddress()); | ||
assertEquals("Correct username is obtained from a credential store", "username", authConfig.getUsername()); | ||
assertEquals("Correct secret is obtained from a credential store", "secret", authConfig.getPassword()); | ||
} | ||
|
||
@Test | ||
public void lookupAuthConfigUsingHelper() throws URISyntaxException { | ||
final RegistryAuthLocator authLocator = createTestAuthLocator("config-with-helper.json"); | ||
|
||
final AuthConfig authConfig = authLocator.lookupAuthConfig(new DockerImageName("registry.example.com/org/repo")); | ||
|
||
assertEquals("Correct server URL is obtained from a credential store", "url", authConfig.getRegistryAddress()); | ||
assertEquals("Correct username is obtained from a credential store", "username", authConfig.getUsername()); | ||
assertEquals("Correct secret is obtained from a credential store", "secret", authConfig.getPassword()); | ||
} | ||
|
||
@NotNull | ||
private RegistryAuthLocator createTestAuthLocator(String configName) throws URISyntaxException { | ||
final File configFile = new File(Resources.getResource("auth-config/" + configName).toURI()); | ||
return new RegistryAuthLocator(new AuthConfig(), configFile, configFile.getParentFile().getAbsolutePath() + "/"); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
{ | ||
"auths": { | ||
"registry.example.com": {} | ||
}, | ||
"HttpHeaders": { | ||
"User-Agent": "Docker-Client/18.03.0-ce (darwin)" | ||
}, | ||
"credsStore": "fake", | ||
"credHelpers": { | ||
"registry.example.com": "fake" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
{ | ||
"auths": { | ||
"registry.example.com": {} | ||
}, | ||
"HttpHeaders": { | ||
"User-Agent": "Docker-Client/18.03.0-ce (darwin)" | ||
}, | ||
"credHelpers": { | ||
"registry.example.com": "fake" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"auths": { | ||
"registry.example.com": {} | ||
}, | ||
"HttpHeaders": { | ||
"User-Agent": "Docker-Client/18.03.0-ce (darwin)" | ||
}, | ||
"credsStore": "fake" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
#!/bin/bash | ||
|
||
if [[ $1 != "get" ]]; then | ||
exit 1 | ||
fi | ||
|
||
read > /dev/null | ||
|
||
echo '{' \ | ||
' "ServerURL": "url",' \ | ||
' "Username": "username",' \ | ||
' "Secret": "secret"' \ | ||
'}' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO this ctor should call