Skip to content

Commit

Permalink
Apply Maven SettingsDecrypter for encrypted server details (#609)
Browse files Browse the repository at this point in the history
  • Loading branch information
briandealwis authored Jul 15, 2018
1 parent cfdc014 commit f065c30
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ public void execute() throws MojoExecutionException {

// Checks Maven settings for registry credentials.
MavenSettingsServerCredentials mavenSettingsServerCredentials =
new MavenSettingsServerCredentials(Preconditions.checkNotNull(session).getSettings());
new MavenSettingsServerCredentials(
Preconditions.checkNotNull(session).getSettings(), settingsDecrypter, mavenBuildLogger);
RegistryCredentials knownBaseRegistryCredentials =
mavenSettingsServerCredentials.retrieve(baseImage.getRegistry());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ public void execute() throws MojoExecutionException, MojoFailureException {

// Checks Maven settings for registry credentials.
MavenSettingsServerCredentials mavenSettingsServerCredentials =
new MavenSettingsServerCredentials(Preconditions.checkNotNull(session).getSettings());
new MavenSettingsServerCredentials(
Preconditions.checkNotNull(session).getSettings(), settingsDecrypter, mavenBuildLogger);
RegistryCredentials knownBaseRegistryCredentials =
mavenSettingsServerCredentials.retrieve(baseImage.getRegistry());
RegistryCredentials knownTargetRegistryCredentials =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ public void execute() throws MojoExecutionException {

// Checks Maven settings for registry credentials.
MavenSettingsServerCredentials mavenSettingsServerCredentials =
new MavenSettingsServerCredentials(Preconditions.checkNotNull(session).getSettings());
new MavenSettingsServerCredentials(
Preconditions.checkNotNull(session).getSettings(), settingsDecrypter, mavenBuildLogger);
RegistryCredentials knownBaseRegistryCredentials =
mavenSettingsServerCredentials.retrieve(baseImage.getRegistry());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,11 @@
import javax.annotation.Nullable;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.settings.crypto.SettingsDecrypter;

/** Defines the configuration parameters for Jib. Jib {@link Mojo}s should extend this class. */
abstract class JibPluginConfiguration extends AbstractMojo {
Expand Down Expand Up @@ -164,6 +166,8 @@ void handleDeprecatedParameters(BuildLogger logger) {
@Parameter(defaultValue = "${project.basedir}/src/main/jib", required = true)
private String extraDirectory;

@Nullable @Component protected SettingsDecrypter settingsDecrypter;

MavenProject getProject() {
return Preconditions.checkNotNull(project);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,17 @@
import com.google.cloud.tools.jib.http.Authorizations;
import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials;
import com.google.common.annotations.VisibleForTesting;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.settings.Server;
import org.apache.maven.settings.Settings;
import org.apache.maven.settings.building.SettingsProblem;
import org.apache.maven.settings.crypto.DefaultSettingsDecryptionRequest;
import org.apache.maven.settings.crypto.SettingsDecrypter;
import org.apache.maven.settings.crypto.SettingsDecryptionRequest;
import org.apache.maven.settings.crypto.SettingsDecryptionResult;

/**
* Retrieves credentials for servers defined in <a
Expand All @@ -31,32 +39,89 @@ class MavenSettingsServerCredentials {

@VisibleForTesting static final String CREDENTIAL_SOURCE = "Maven settings";

// pattern cribbed directly from
// https://github.com/sonatype/plexus-cipher/blob/master/src/main/java/org/sonatype/plexus/components/cipher/DefaultPlexusCipher.java
private static final Pattern ENCRYPTED_STRING_PATTERN =
Pattern.compile(".*?[^\\\\]?\\{(.*?[^\\\\])\\}.*");

/**
* Return true if the given string appears to have been encrypted with the <a
* href="https://maven.apache.org/guides/mini/guide-encryption.html#How_to_encrypt_server_passwords">Maven
* password encryption</a>. Such passwords appear between unescaped braces.
*/
@VisibleForTesting
static boolean isEncrypted(String password) {
Matcher matcher = ENCRYPTED_STRING_PATTERN.matcher(password);
return matcher.matches() || matcher.find();
}

private final Settings settings;
@Nullable private final SettingsDecrypter settingsDecrypter;
private final MavenBuildLogger mavenBuildLogger;

MavenSettingsServerCredentials(Settings settings) {
/**
* Create new instance.
*
* @param settings the Maven settings object
* @param settingsDecrypter the Maven decrypter component
* @param mavenBuildLogger the Maven build log
*/
MavenSettingsServerCredentials(
Settings settings,
@Nullable SettingsDecrypter settingsDecrypter,
MavenBuildLogger mavenBuildLogger) {
this.settings = settings;
this.settingsDecrypter = settingsDecrypter;
this.mavenBuildLogger = mavenBuildLogger;
}

/**
* Attempts to retrieve credentials for {@code registry} from Maven settings.
*
* @param registry the registry
* @return the credentials for the registry
* @throws MojoExecutionException if the credentials could not be retrieved
*/
@Nullable
RegistryCredentials retrieve(@Nullable String registry) {
RegistryCredentials retrieve(@Nullable String registry) throws MojoExecutionException {
if (registry == null) {
return null;
}

Server registryServerSettings = settings.getServer(registry);
if (registryServerSettings == null) {
Server registryServer = settings.getServer(registry);
if (registryServer == null) {
return null;
}

if (settingsDecrypter != null) {
// SettingsDecrypter and SettingsDecryptionResult do not document the meanings of the return
// results. SettingsDecryptionResult#getServers() does note that the list of decrypted servers
// can be empty. We handle the results as follows:
// - if there are any ERROR or FATAL problems reported, then decryption failed
// - if no decrypted servers returned then treat as if no decryption was required
SettingsDecryptionRequest request = new DefaultSettingsDecryptionRequest(registryServer);
SettingsDecryptionResult result = settingsDecrypter.decrypt(request);
// un-encrypted passwords are passed through, so a problem indicates a real issue
for (SettingsProblem problem : result.getProblems()) {
if (problem.getSeverity() == SettingsProblem.Severity.ERROR
|| problem.getSeverity() == SettingsProblem.Severity.FATAL) {
throw new MojoExecutionException(
"Unable to decrypt password for " + registry + ": " + problem);
}
}
if (result.getServer() != null) {
registryServer = result.getServer();
}
} else if (isEncrypted(registryServer.getPassword())) {
mavenBuildLogger.warn(
"Server password for registry "
+ registry
+ " appears to be encrypted, but there is no decrypter available");
}

return new RegistryCredentials(
CREDENTIAL_SOURCE,
Authorizations.withBasicCredentials(
registryServerSettings.getUsername(), registryServerSettings.getPassword()));
registryServer.getUsername(), registryServer.getPassword()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,13 @@
import com.google.cloud.tools.jib.http.Authorization;
import com.google.cloud.tools.jib.http.Authorizations;
import com.google.cloud.tools.jib.registry.credentials.RegistryCredentials;
import java.util.Collections;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.settings.Server;
import org.apache.maven.settings.Settings;
import org.apache.maven.settings.building.SettingsProblem;
import org.apache.maven.settings.crypto.SettingsDecrypter;
import org.apache.maven.settings.crypto.SettingsDecryptionResult;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
Expand All @@ -35,16 +40,18 @@ public class MavenSettingsServerCredentialsTest {

@Mock private Settings mockSettings;
@Mock private Server mockServer1;
@Mock private MavenBuildLogger mockLogger;

private MavenSettingsServerCredentials testMavenSettingsServerCredentials;

@Before
public void setUp() {
testMavenSettingsServerCredentials = new MavenSettingsServerCredentials(mockSettings);
testMavenSettingsServerCredentials =
new MavenSettingsServerCredentials(mockSettings, null, mockLogger);
}

@Test
public void testRetrieve_found() {
public void testRetrieve_found() throws MojoExecutionException {
Mockito.when(mockSettings.getServer("server1")).thenReturn(mockServer1);

Mockito.when(mockServer1.getUsername()).thenReturn("server1 username");
Expand All @@ -63,20 +70,131 @@ public void testRetrieve_found() {
Assert.assertEquals(
Authorizations.withBasicCredentials("server1 username", "server1 password").toString(),
retrievedServer1Authorization.toString());
Mockito.verifyZeroInteractions(mockLogger);
}

@Test
public void testRetrieve_notFound() {
public void testRetrieve_notFound() throws MojoExecutionException {
RegistryCredentials registryCredentials =
testMavenSettingsServerCredentials.retrieve("serverUnknown");

Assert.assertNull(registryCredentials);
}

@Test
public void testRetrieve_withNullServer() {
public void testRetrieve_withNullServer() throws MojoExecutionException {
RegistryCredentials registryCredentials = testMavenSettingsServerCredentials.retrieve(null);

Assert.assertNull(registryCredentials);
}

@Test
public void testRetrieve_withNullDecrypter_encrypted() throws MojoExecutionException {
Mockito.when(mockSettings.getServer("server1")).thenReturn(mockServer1);
Mockito.when(mockServer1.getUsername()).thenReturn("server1 username");
Mockito.when(mockServer1.getPassword()).thenReturn("{COQLCE6DU6GtcS5P=}");

RegistryCredentials registryCredentials =
testMavenSettingsServerCredentials.retrieve("server1");

Assert.assertNotNull(registryCredentials);
Assert.assertEquals(
MavenSettingsServerCredentials.CREDENTIAL_SOURCE,
registryCredentials.getCredentialSource());

Authorization retrievedServer1Authorization = registryCredentials.getAuthorization();
Assert.assertNotNull(retrievedServer1Authorization);
Assert.assertEquals(
Authorizations.withBasicCredentials("server1 username", "{COQLCE6DU6GtcS5P=}").toString(),
retrievedServer1Authorization.toString());
Mockito.verify(mockLogger)
.warn(
"Server password for registry server1 appears to be encrypted, "
+ "but there is no decrypter available");
}

@Test
public void testRetrieve_withDecrypter_success() throws MojoExecutionException {
SettingsDecryptionResult mockResult = Mockito.mock(SettingsDecryptionResult.class);
Mockito.when(mockResult.getProblems()).thenReturn(Collections.emptyList());
Mockito.when(mockResult.getServer()).thenReturn(mockServer1);

// don't actually perform encryption/decryption
SettingsDecrypter mockDecrypter = Mockito.mock(SettingsDecrypter.class);
Mockito.when(mockDecrypter.decrypt(Mockito.any())).thenReturn(mockResult);
testMavenSettingsServerCredentials =
new MavenSettingsServerCredentials(mockSettings, mockDecrypter, mockLogger);

// essentially the same as testRetrieve_found()
Mockito.when(mockSettings.getServer("server1")).thenReturn(mockServer1);
Mockito.when(mockServer1.getUsername()).thenReturn("server1 username");
Mockito.when(mockServer1.getPassword()).thenReturn("server1 password");

RegistryCredentials registryCredentials =
testMavenSettingsServerCredentials.retrieve("server1");

Assert.assertNotNull(registryCredentials);
Assert.assertEquals(
MavenSettingsServerCredentials.CREDENTIAL_SOURCE,
registryCredentials.getCredentialSource());

Authorization retrievedServer1Authorization = registryCredentials.getAuthorization();
Assert.assertNotNull(retrievedServer1Authorization);
Assert.assertEquals(
Authorizations.withBasicCredentials("server1 username", "server1 password").toString(),
retrievedServer1Authorization.toString());

Mockito.verify(mockDecrypter).decrypt(Mockito.any());
Mockito.verify(mockResult).getProblems();
Mockito.verify(mockResult, Mockito.atLeastOnce()).getServer();
}

@Test
public void testRetrieve_withDecrypter_failure() {

SettingsProblem mockProblem = Mockito.mock(SettingsProblem.class);
Mockito.when(mockProblem.getSeverity()).thenReturn(SettingsProblem.Severity.ERROR);
// Maven's SettingsProblem has a more structured toString, but irrelevant here
Mockito.when(mockProblem.toString()).thenReturn("MockProblemText");

SettingsDecryptionResult mockResult = Mockito.mock(SettingsDecryptionResult.class);
Mockito.when(mockResult.getProblems()).thenReturn(Collections.singletonList(mockProblem));

// return an result with problems
SettingsDecrypter mockDecrypter = Mockito.mock(SettingsDecrypter.class);
Mockito.when(mockDecrypter.decrypt(Mockito.any())).thenReturn(mockResult);
testMavenSettingsServerCredentials =
new MavenSettingsServerCredentials(mockSettings, mockDecrypter, mockLogger);

// essentially the same as testRetrieve_found()
Mockito.when(mockSettings.getServer("server1")).thenReturn(mockServer1);

try {
testMavenSettingsServerCredentials.retrieve("server1");
Assert.fail("decryption should have failed");
} catch (MojoExecutionException ex) {
Assert.assertEquals(
ex.getMessage(), "Unable to decrypt password for server1: MockProblemText");
Mockito.verify(mockDecrypter).decrypt(Mockito.any());
Mockito.verify(mockResult).getProblems();
Mockito.verifyNoMoreInteractions(mockResult); // getServer() should never be called
}
}

@Test
public void testIsEncrypted_plaintext() {
Assert.assertFalse(MavenSettingsServerCredentials.isEncrypted("plain text"));
}

@Test
public void testIsEncrypted_encryptedPayload() {
String examples[] = {
"{COQLCE6DU6GtcS5P=}",
"expires on 2009-04-11 {COQLCE6DU6GtcS5P=}", // with note
"{jSMOWnoPFgsHVpMvz5VrIt5kRbzGpI8u+\\{EF1iFQyJQ=}" // with escaped brace
};
for (String payload : examples) {
Assert.assertTrue(MavenSettingsServerCredentials.isEncrypted(payload));
}
}
}

0 comments on commit f065c30

Please sign in to comment.