diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 3a40cdea6b..0954b7b7a2 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -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()); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index 3159bc05fb..ac3d6aec63 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -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 = diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java index f4670b022b..e942fafc21 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildTarMojo.java @@ -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()); diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java index 4fb8108931..3d0ad68e17 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/JibPluginConfiguration.java @@ -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 { @@ -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); } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java index 1e2d4fd397..0501bdfbb6 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentials.java @@ -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 Maven + * password encryption. 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; } /** @@ -42,21 +80,48 @@ class MavenSettingsServerCredentials { * * @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())); } } diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java index 4fa507cde0..8b03444ee4 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/MavenSettingsServerCredentialsTest.java @@ -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; @@ -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"); @@ -63,10 +70,11 @@ 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"); @@ -74,9 +82,119 @@ public void testRetrieve_notFound() { } @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)); + } + } }