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

use credential helper if defined #821

Merged
merged 2 commits into from
Nov 3, 2017
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
3 changes: 2 additions & 1 deletion doc/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

* **0.22-SNAPSHOT**
- Support relative paths when binding volumes in `docker-compose.yml` (#846)
- Allo the session token for AWS authetication to be oncluded in order to allow temporary security credentials provided by the AWS Security Token Service (AWS STS) to sign requests (#883)
- Allow the session token for AWS authetication to be oncluded in order to allow temporary security credentials provided by the AWS Security Token Service (AWS STS) to sign requests (#883)
- Add support for credential helper to authenticate against a registry (#821)

* **0.22.1** (2017-08-28)
- Allow Docker compose version "2", too ([#829](https://github.com/fabric8io/docker-maven-plugin/issues/829))
Expand Down
15 changes: 10 additions & 5 deletions src/main/asciidoc/inc/_authentication.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ There are five different locations searched for credentials. In order, these ar
* Using a `<authConfig>` section in the plugin configuration with `<username>` and `<password>` elements.
* Using OpenShift configuration in `~/.config/kube`
* Using a `<server>` configuration in `~/.m2/settings.xml`
* Login into a registry with `docker login` (credentials in `~/.docker/config.json`)
* Login into a registry with `docker login` (credentials in a credential helper or in `~/.docker/config.json`)

Using the username and password directly in the `pom.xml` is not
recommended since this is widely visible. This is most easiest and
Expand Down Expand Up @@ -41,8 +41,7 @@ the outside:
mvn -Ddocker.username=jolokia -Ddocker.password=s!cr!t {plugin}:push
----

The most secure and also the most _mavenish_ way is to add a server to
the Maven settings file `~/.m2/settings.xml`:
The most _mavenish_ way is to add a server to the Maven settings file `~/.m2/settings.xml`:

.Example
[source,xml]
Expand All @@ -65,8 +64,14 @@ have a second account 'fabric8io' then use an `<id>docker.io/fabric8io</id>` for
username with a slash to the id name. The default without username is only taken if no server entry with
a username appended id is chosen.

As a final fallback, this plugin consults `~/.docker/config.json` for getting to the credentials. Within this
file credentials are stored when connecting to a registry with the command `docker login` from the command line.
The most _secure_ way is to rely on docker's credential store or credential helper and read confidential information
from an external credentials store, such as the native keychain of the operating system. Follow the instruction on
https://docs.docker.com/engine/reference/commandline/login/#credentials-store[the docker login documentation].

As a final fallback, this plugin consults `~/.docker/config.json` and reads credentials stored directly within this
file. This unsafe behavior happened when connecting to a registry with the command `docker login` from the command line
with older versions of docker (pre 1.13.0) or when docker is not configured to use a
https://docs.docker.com/engine/reference/commandline/login/#credentials-store[credential store].

== Pull vs. Push Authentication

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,15 @@ public ExternalCommand(Logger log) {
}

public void execute() throws IOException {
execute(null);
}

public void execute(String processInput) throws IOException {
final Process process = startProcess();
start();
try {
closeOutputStream(process.getOutputStream());
inputStreamPump(process.getOutputStream(),processInput);

Future<IOException> stderrFuture = startStreamPump(process.getErrorStream());
outputStreamPump(process.getInputStream());

Expand Down Expand Up @@ -71,24 +76,27 @@ protected int getStatusCode() {

private void checkProcessExit(Process process) {
try {
statusCode = process.waitFor();
executor.shutdown();
executor.awaitTermination(10, TimeUnit.SECONDS);
statusCode = process.exitValue();
} catch (IllegalThreadStateException | InterruptedException e) {
process.destroy();
statusCode = -1;
}
}

private void closeOutputStream(OutputStream outputStream) {
try {
outputStream.close();
private void inputStreamPump(OutputStream outputStream,String processInput) throws IOException {
try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream))) {
if (processInput != null) {
writer.write(processInput);
writer.flush();
}
} catch (IOException e) {
log.info("Failed to close process output stream: %s", e.getMessage());
}
}

private Process startProcess(String... args) throws IOException {
private Process startProcess() throws IOException {
try {
return Runtime.getRuntime().exec(getArgs());
} catch (IOException e) {
Expand All @@ -98,14 +106,14 @@ private Process startProcess(String... args) throws IOException {
}
}

private String getCommandAsString() {
protected String getCommandAsString() {
return StringUtils.join(getArgs(), " ");
}

protected abstract String[] getArgs();

private void outputStreamPump(final InputStream inputStream) throws IOException {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
for (; ; ) {
String line = reader.readLine();
if (line == null) {
Expand Down
37 changes: 33 additions & 4 deletions src/main/java/io/fabric8/maven/docker/util/AuthConfigFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -308,12 +308,33 @@ private AuthConfig getAuthConfigFromSettings(Settings settings, String user, Str

private AuthConfig getAuthConfigFromDockerConfig(String registry) throws MojoExecutionException {
JSONObject dockerConfig = readDockerConfig();
if (dockerConfig == null || !dockerConfig.has("auths")) {
if (dockerConfig == null) {
return null;
}
JSONObject auths = dockerConfig.getJSONObject("auths");
String registryToLookup = registry != null ? registry : DOCKER_LOGIN_DEFAULT_REGISTRY;
JSONObject credentials = getCredentialsNode(auths, registryToLookup);

if (dockerConfig.has("credHelpers") || dockerConfig.has("credsStore")) {
if (dockerConfig.has("credHelpers")) {
final JSONObject credHelpers = dockerConfig.getJSONObject("credHelpers");
if (credHelpers.has(registryToLookup)) {
return extractAuthConfigFromCredentialsHelper(registryToLookup, credHelpers.getString(registryToLookup));
}
}
if (dockerConfig.has("credsStore")) {
return extractAuthConfigFromCredentialsHelper(registryToLookup, dockerConfig.getString("credsStore"));
}
return null;
}

if (dockerConfig.has("auths")) {
return extractAuthConfigFromAuths(registryToLookup, dockerConfig.getJSONObject("auths"));
}

return null;
}

private AuthConfig extractAuthConfigFromAuths(String registryToLookup, JSONObject auths) {
JSONObject credentials = getCredentialsNode(auths,registryToLookup);
if (credentials == null || !credentials.has("auth")) {
return null;
}
Expand All @@ -322,7 +343,15 @@ private AuthConfig getAuthConfigFromDockerConfig(String registry) throws MojoExe
return new AuthConfig(auth,email);
}

private JSONObject getCredentialsNode(JSONObject auths, String registryToLookup) {
private AuthConfig extractAuthConfigFromCredentialsHelper(String registryToLookup, String credConfig) throws MojoExecutionException {
CredentialHelperClient credentialHelper = new CredentialHelperClient(log, credConfig);
log.debug("AuthConfig: credentials from credential helper/store %s version %s",
credentialHelper.getName(),
credentialHelper.getVersion());
return credentialHelper.getAuthConfig(registryToLookup);
}

private JSONObject getCredentialsNode(JSONObject auths,String registryToLookup) {
if (auths.has(registryToLookup)) {
return auths.getJSONObject(registryToLookup);
}
Expand Down
118 changes: 118 additions & 0 deletions src/main/java/io/fabric8/maven/docker/util/CredentialHelperClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package io.fabric8.maven.docker.util;

import io.fabric8.maven.docker.access.AuthConfig;
import io.fabric8.maven.docker.access.util.ExternalCommand;
import org.apache.maven.plugin.MojoExecutionException;
import org.json.JSONObject;
import org.json.JSONTokener;

import java.io.IOException;

public class CredentialHelperClient {

static final String SECRET_KEY = "Secret";
static final String USERNAME_KEY = "Username";
private final String credentialHelperName;
private final Logger log;

public CredentialHelperClient(Logger log, String credentialsStore) {
this.log = log;
credentialHelperName = "docker-credential-" + credentialsStore;
}

public String getName() {
return credentialHelperName;
}

public String getVersion() throws MojoExecutionException {
try {
return new VersionCommand().getVersion();
} catch (IOException e) {
throw new MojoExecutionException("Error getting the version of the configured credential helper",e);
}
}

public AuthConfig getAuthConfig(String registryToLookup) throws MojoExecutionException {
try {
final GetCommand getCommand = new GetCommand();
return toAuthConfig(getCommand.getCredentialNode("https://" + registryToLookup));
} catch (IOException e) {
throw new MojoExecutionException("Error getting the credentials for " + registryToLookup + " from the configured credential helper",e);
}
}

private AuthConfig toAuthConfig(JSONObject credential){
if (credential == null) {
return null;
}
String password = credential.getString(CredentialHelperClient.SECRET_KEY);
String userKey = credential.getString(CredentialHelperClient.USERNAME_KEY);
return new AuthConfig(userKey,password, null,null);
}

// docker-credential-XXX version
private class VersionCommand extends ExternalCommand {

private String version;

VersionCommand() {
super(CredentialHelperClient.this.log);
}

@Override
protected String[] getArgs() {
return new String[]{CredentialHelperClient.this.credentialHelperName,"version"};
}

@Override
protected void processLine(String line) {
log.info("Credentials helper reply for \"%s\" is %s",CredentialHelperClient.this.credentialHelperName,line);
version = line;
}

public String getVersion() throws IOException {
execute();
if (version == null) {
throw new IOException("No reply information returned by " + getCommandAsString());
}
return version;
}
}

// echo <registryToLookup> | docker-credential-XXX get
private class GetCommand extends ExternalCommand {

private String reply;

GetCommand() {
super(CredentialHelperClient.this.log);
}

@Override
protected String[] getArgs() {
return new String[]{CredentialHelperClient.this.credentialHelperName,"get"};
}

@Override
protected void processLine(String line) {
reply = line;
}

public JSONObject getCredentialNode(String registryToLookup) throws IOException {
try {
execute(registryToLookup);
} catch (IOException ex) {
if (getStatusCode() == 1) {
return null;
} else {
throw ex;
}
}
JSONObject credentials = new JSONObject(new JSONTokener(reply));
if (!credentials.has(SECRET_KEY) || !credentials.has(USERNAME_KEY)) {
return null;
}
return credentials;
}
}
}
Loading