Skip to content

Commit

Permalink
Add provisioning options to modify the GPG settings
Browse files Browse the repository at this point in the history
  • Loading branch information
spyrkob committed Oct 11, 2024
1 parent 1ddb35c commit d0bcc2b
Show file tree
Hide file tree
Showing 14 changed files with 284 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,15 @@

import java.io.File;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

import org.assertj.core.api.Assertions;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.jboss.galleon.api.config.GalleonProvisioningConfig;
import org.jboss.galleon.universe.FeaturePackLocation;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
Expand All @@ -37,16 +40,19 @@
import org.wildfly.channel.Stream;
import org.wildfly.channel.spi.SignatureResult;
import org.wildfly.channel.spi.SignatureValidator;
import org.wildfly.prospero.actions.ProvisioningAction;
import org.wildfly.prospero.api.MavenOptions;
import org.wildfly.prospero.it.AcceptingConsole;
import org.wildfly.prospero.metadata.ProsperoMetadataUtils;
import org.wildfly.prospero.signatures.KeystoreManager;
import org.wildfly.prospero.signatures.PGPLocalKeystore;
import org.wildfly.prospero.test.BuildProperties;
import org.wildfly.prospero.test.CertificateUtils;
import org.wildfly.prospero.test.TestInstallation;
import org.wildfly.prospero.test.TestLocalRepository;

public class InstallationTestCase {

// TODO: missing use case - install using custom keyring
@Rule
public TemporaryFolder temp = new TemporaryFolder();
private TestLocalRepository testLocalRepository;
Expand Down Expand Up @@ -173,6 +179,45 @@ public boolean acceptPublicKey(String key) {
CertificateUtils.assertKeystoreIsEmpty(serverPath.resolve(ProsperoMetadataUtils.METADATA_DIR).resolve("keyring.gpg"));
}

@Test
public void installUsingCustomKeystore_AcceptsKnownCerts() throws Exception {
// prepare a keyring with imported certificate
final Path keyring = temp.newFile("keystore.gpg").toPath();
Files.delete(keyring);
try (PGPLocalKeystore pgpLocalKeystore = KeystoreManager.keystoreFor(keyring);) {
pgpLocalKeystore.importCertificate(List.of(pgpValidKeys.getPublicKey()));
}

// create a channel that requires the GPG checks but has no certificate URLs information
final Channel testChannel = new Channel.Builder()
.setName("test-channel")
.setGpgCheck(true)
.addRepository("local-repo", testLocalRepository.getUri().toString())
.setManifestCoordinate("org.test", "test-channel", "1.0.0")
.build();

// create a console that will reject any new signatures
final AcceptingConsole rejectingConsole = new AcceptingConsole() {
@Override
public boolean acceptPublicKey(String key) {
return false;
}
};

// finally, provision the server using keyring created at the beginning
try (ProvisioningAction action = new ProvisioningAction(serverPath, MavenOptions.OFFLINE_NO_CACHE, keyring, rejectingConsole)) {
action.provision(GalleonProvisioningConfig.builder()
.addFeaturePackDep(FeaturePackLocation.fromString("org.test:pack-one:1.0.0"))
.build(), List.of(testChannel));

}

// and verify we did install the server
testInstallation.verifyModuleJar("commons-io", "commons-io", "2.16.1");
testInstallation.verifyInstallationMetadataPresent();
CertificateUtils.assertKeystoreContains(serverPath.resolve(ProsperoMetadataUtils.METADATA_DIR).resolve("keyring.gpg"), pgpValidKeys.getPublicKey().getKeyID());
}

private void prepareRequiredArtifacts(TestLocalRepository localRepository) throws Exception {
final String galleonPluginsVersion = BuildProperties.getProperty("version.org.wildfly.galleon-plugins");
final String commonsIoVersion = BuildProperties.getProperty("version.commons-io");
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
<version.org.yaml.snakeyaml>2.2</version.org.yaml.snakeyaml>
<version.junit>4.13.2</version.junit>
<version.maven-shade-plugin>3.6.0</version.maven-shade-plugin>
<version.org.wildfly.channel>1.1.1.Final-SNAPSHOT</version.org.wildfly.channel>
<version.org.wildfly.channel>2.0.0.Final-SNAPSHOT</version.org.wildfly.channel>
<version.maven-compiler-plugin>3.10.1</version.maven-compiler-plugin>
<version.org.wildfly.galleon-pack>33.0.1.Final</version.org.wildfly.galleon-pack>
<version.info.picocli>4.7.6</version.info.picocli>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@

public class ActionFactory {

public ProvisioningAction install(Path targetPath, MavenOptions mavenOptions, Console console) throws ProvisioningException {
return new ProvisioningAction(targetPath, mavenOptions, console);
public ProvisioningAction install(Path targetPath, MavenOptions mavenOptions, Path keystorePath, Console console) throws ProvisioningException {
return new ProvisioningAction(targetPath, mavenOptions, keystorePath, console);
}

// Option for BETA update support
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -800,4 +800,8 @@ default String certificateRevokePrompt() {
default String certificateRevoked() {
return bundle.getString("prospero.certificate.revoke.success");
}

default ArgumentParsingException unableToReadKeyring(Path keyring, Exception cause) {
return new ArgumentParsingException(String.format("Unable to parse GPG keyring at %s: %s", keyring, cause.getMessage()), cause);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ private Commands() {
public static final String FEATURE_PACK_REFERENCE = "<feature-pack-reference>";
public static final String FPL = "--fpl";
public static final String GPG_CHECK = "--gpg-check";
public static final String GPG_KEYSTORE = "--gpg-keystore";
public static final String H = "-h";
public static final String HELP = "--help";
public static final String KEY_ID= "--key-id";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Iterator;
Expand Down Expand Up @@ -60,6 +61,7 @@
import org.wildfly.prospero.galleon.GalleonUtils;
import org.wildfly.prospero.licenses.License;
import org.wildfly.prospero.model.InstallationProfile;
import org.wildfly.prospero.signatures.KeystoreManager;
import picocli.CommandLine;

import javax.xml.stream.XMLStreamException;
Expand Down Expand Up @@ -96,6 +98,16 @@ public class InstallCommand extends AbstractInstallCommand {
)
List<String> shadowRepositories = new ArrayList<>();

@CommandLine.Option(
names = CliConstants.GPG_CHECK
)
Optional<Boolean> requireGpgCheck;

@CommandLine.Option(
names = CliConstants.GPG_KEYSTORE
)
Path gpgKeystore;

protected static final List<String> STABILITY_LEVELS = List.of(Constants.STABILITY_EXPERIMENTAL,
Constants.STABILITY_PREVIEW,
Constants.STABILITY_DEFAULT,
Expand Down Expand Up @@ -186,6 +198,17 @@ public Integer call() throws Exception {
}
}

if (gpgKeystore != null) {
if (!Files.exists(gpgKeystore)) {
throw CliMessages.MESSAGES.certificateNonExistingFilePath(gpgKeystore);
}
try {
KeystoreManager.keystoreFor(gpgKeystore).close();
} catch (Exception e) {
throw CliMessages.MESSAGES.unableToReadKeyring(gpgKeystore, e);
}
}

stabilityLevels.verify();

if (featurePackOrDefinition.definition.isPresent()) {
Expand All @@ -198,6 +221,7 @@ public Integer call() throws Exception {
.setStabilityLevel(stabilityLevels.stabilityLevel==null?null:stabilityLevels.stabilityLevel.toLowerCase(Locale.ROOT))
.setPackageStabilityLevel(stabilityLevels.packageStabilityLevel==null?null:stabilityLevels.packageStabilityLevel.toLowerCase(Locale.ROOT))
.setConfigStabilityLevel(stabilityLevels.configStabilityLevel==null?null:stabilityLevels.configStabilityLevel.toLowerCase(Locale.ROOT))
.setRequireGpgCheck(requireGpgCheck.orElse(null))
.build();
final MavenOptions mavenOptions = getMavenOptions();
final GalleonProvisioningConfig provisioningConfig = provisioningDefinition.toProvisioningConfig();
Expand All @@ -206,51 +230,52 @@ public Integer call() throws Exception {
List<Repository> repositories = RepositoryDefinition.from(this.shadowRepositories);
final List<Repository> shadowRepositories = RepositoryUtils.unzipArchives(repositories, temporaryFiles);

final ProvisioningAction provisioningAction = actionFactory.install(directory.toAbsolutePath(), mavenOptions,
console);

if (featurePackOrDefinition.fpl.isPresent()) {
console.println(CliMessages.MESSAGES.installingFpl(featurePackOrDefinition.fpl.get()));
} else if (featurePackOrDefinition.profile.isPresent()) {
console.println(CliMessages.MESSAGES.installingProfile(featurePackOrDefinition.profile.get()));
} else if (featurePackOrDefinition.definition.isPresent()) {
console.println(CliMessages.MESSAGES.installingDefinition(featurePackOrDefinition.definition.get()));
}
try (ProvisioningAction provisioningAction = actionFactory.install(directory.toAbsolutePath(), mavenOptions,
gpgKeystore, console)) {

if (featurePackOrDefinition.fpl.isPresent()) {
console.println(CliMessages.MESSAGES.installingFpl(featurePackOrDefinition.fpl.get()));
} else if (featurePackOrDefinition.profile.isPresent()) {
console.println(CliMessages.MESSAGES.installingProfile(featurePackOrDefinition.profile.get()));
} else if (featurePackOrDefinition.definition.isPresent()) {
console.println(CliMessages.MESSAGES.installingDefinition(featurePackOrDefinition.definition.get()));
}

final List<Channel> effectiveChannels = TemporaryRepositoriesHandler.overrideRepositories(channels, shadowRepositories);
console.println(CliMessages.MESSAGES.usingChannels());
final ChannelPrinter channelPrinter = new ChannelPrinter(console);
for (Channel channel : effectiveChannels) {
channelPrinter.print(channel);
}

console.println("");
final List<Channel> effectiveChannels = TemporaryRepositoriesHandler.overrideRepositories(channels, shadowRepositories);
console.println(CliMessages.MESSAGES.usingChannels());
final ChannelPrinter channelPrinter = new ChannelPrinter(console);
for (Channel channel : effectiveChannels) {
channelPrinter.print(channel);
}

final List<License> pendingLicenses = provisioningAction.getPendingLicenses(provisioningConfig,
effectiveChannels);
if (!pendingLicenses.isEmpty()) {
new LicensePrinter(console).print(pendingLicenses);
console.println("");
if (acceptAgreements) {
console.println(CliMessages.MESSAGES.agreementSkipped(CliConstants.ACCEPT_AGREEMENTS));

final List<License> pendingLicenses = provisioningAction.getPendingLicenses(provisioningConfig,
effectiveChannels);
if (!pendingLicenses.isEmpty()) {
new LicensePrinter(console).print(pendingLicenses);
console.println("");
} else {
if (!console.confirm(CliMessages.MESSAGES.acceptAgreements(), "", CliMessages.MESSAGES.installationCancelled())) {
return ReturnCodes.PROCESSING_ERROR;
if (acceptAgreements) {
console.println(CliMessages.MESSAGES.agreementSkipped(CliConstants.ACCEPT_AGREEMENTS));
console.println("");
} else {
if (!console.confirm(CliMessages.MESSAGES.acceptAgreements(), "", CliMessages.MESSAGES.installationCancelled())) {
return ReturnCodes.PROCESSING_ERROR;
}
}
}
}

provisioningAction.provision(provisioningConfig, channels, shadowRepositories);
provisioningAction.provision(provisioningConfig, channels, shadowRepositories);

console.println("");
console.println(CliMessages.MESSAGES.installComplete(directory));
console.println("");
console.println(CliMessages.MESSAGES.installComplete(directory));

final float totalTime = (System.currentTimeMillis() - startTime) / 1000f;
console.println(CliMessages.MESSAGES.operationCompleted(totalTime));
final float totalTime = (System.currentTimeMillis() - startTime) / 1000f;
console.println(CliMessages.MESSAGES.operationCompleted(totalTime));

return ReturnCodes.SUCCESS;
return ReturnCodes.SUCCESS;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,20 +57,21 @@ public Integer call() throws Exception {
final GalleonProvisioningConfig provisioningConfig = provisioningDefinition.toProvisioningConfig();
final List<Channel> channels = ChannelUtils.resolveChannels(provisioningDefinition, mavenOptions);

final ProvisioningAction provisioningAction = actionFactory.install(tempDirectory.toAbsolutePath(),
mavenOptions, console);
try (ProvisioningAction provisioningAction = actionFactory.install(tempDirectory.toAbsolutePath(),
mavenOptions, null, console)) {

final List<License> pendingLicenses = provisioningAction.getPendingLicenses(provisioningConfig, channels);
if (!pendingLicenses.isEmpty()) {
console.println("");
console.println(CliMessages.MESSAGES.listAgreementsHeader());
console.println("");
new LicensePrinter(console).print(pendingLicenses);
} else {
console.println("");
console.println(CliMessages.MESSAGES.noAgreementsNeeded());
final List<License> pendingLicenses = provisioningAction.getPendingLicenses(provisioningConfig, channels);
if (!pendingLicenses.isEmpty()) {
console.println("");
console.println(CliMessages.MESSAGES.listAgreementsHeader());
console.println("");
new LicensePrinter(console).print(pendingLicenses);
} else {
console.println("");
console.println(CliMessages.MESSAGES.noAgreementsNeeded());
}
return ReturnCodes.SUCCESS;
}
return ReturnCodes.SUCCESS;
} finally {
FileUtils.deleteQuietly(tempDirectory.toFile());
}
Expand Down
1 change: 1 addition & 0 deletions prospero-cli/src/main/resources/UsageMessages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ certificate-file = Path to the file containing armored public key GPG certificat
key-id = The key ID of the public key to be removed. The key needs to be in a hexadecimal form.
revoke-certificate = Path to the file containing armored revocation certificate of a public key.
gpg-check = Require all artifacts from this channel to be GPG verified.
gpg-keystore = Path to a GPG keystore that should be used to verify downloaded artifacts.

${prospero.dist.name}.update.prepare.candidate-dir = Target directory where the candidate server will be provisioned. The existing server is not updated.
${prospero.dist.name}.update.subscribe.product = Specify the product name. This must be a known feature pack supported by ${prospero.dist.name}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;

@RunWith(MockitoJUnitRunner.class)
Expand Down Expand Up @@ -93,7 +94,7 @@ protected ActionFactory createActionFactory() {
@Before
public void setUp() throws Exception {
super.setUp();
when(actionFactory.install(any(), any(), any())).thenReturn(provisionAction);
when(actionFactory.install(any(), any(), any(), any())).thenReturn(provisionAction);
}

@Test
Expand Down Expand Up @@ -446,9 +447,63 @@ public void multipleManifestsAreTranslatedToMultipleChannels() throws Exception
);
}

@Test
public void setGpgIsPassedToAction() throws Exception {
int exitCode = commandLine.execute(CliConstants.Commands.INSTALL, CliConstants.DIR, "test",
CliConstants.FPL, "org.wildfly:wildfly-ee-galleon-pack",
CliConstants.CHANNEL_MANIFEST, "test:test-manifest",
CliConstants.REPOSITORIES, "test::http://test.te",
CliConstants.GPG_CHECK);
assertEquals(ReturnCodes.SUCCESS, exitCode);
Mockito.verify(provisionAction).provision(any(), channelCaptor.capture(), any());
assertThat(channelCaptor.getValue())
.map(Channel::isGpgCheck)
.containsOnly(true);
}

@Test
public void keystoreToBeUsedIsPassedToAction() throws Exception {
final File keystoreFile = temporaryFolder.newFile("keystore.gpg");
int exitCode = commandLine.execute(CliConstants.Commands.INSTALL, CliConstants.DIR, "test",
CliConstants.FPL, "org.wildfly:wildfly-ee-galleon-pack",
CliConstants.CHANNEL_MANIFEST, "test:test-manifest",
CliConstants.REPOSITORIES, "test::http://test.te",
CliConstants.GPG_KEYSTORE, keystoreFile.getAbsolutePath());
assertEquals(ReturnCodes.SUCCESS, exitCode);
Mockito.verify(actionFactory).install(any(), any(), eq(keystoreFile.toPath()), any());
}

@Test
public void validateKeystoreExists() throws Exception {
final Path keystoreFile = temporaryFolder.getRoot().toPath().resolve("idontexist.gpg");
int exitCode = commandLine.execute(CliConstants.Commands.INSTALL, CliConstants.DIR, "test",
CliConstants.FPL, "org.wildfly:wildfly-ee-galleon-pack",
CliConstants.CHANNEL_MANIFEST, "test:test-manifest",
CliConstants.REPOSITORIES, "test::http://test.te",
CliConstants.GPG_KEYSTORE, keystoreFile.toString());
assertEquals(ReturnCodes.INVALID_ARGUMENTS, exitCode);
assertThat(getErrorOutput())
.contains(CliMessages.MESSAGES.certificateNonExistingFilePath(keystoreFile).getMessage());
}

@Test
public void validateKeystoreIsAValidKeystore() throws Exception {
final Path keystoreFile = temporaryFolder.newFile("keystore.gpg").toPath();
Files.writeString(keystoreFile, "some rubbish");

int exitCode = commandLine.execute(CliConstants.Commands.INSTALL, CliConstants.DIR, "test",
CliConstants.FPL, "org.wildfly:wildfly-ee-galleon-pack",
CliConstants.CHANNEL_MANIFEST, "test:test-manifest",
CliConstants.REPOSITORIES, "test::http://test.te",
CliConstants.GPG_KEYSTORE, keystoreFile.toString());
assertEquals(ReturnCodes.INVALID_ARGUMENTS, exitCode);
assertThat(getErrorOutput())
.contains(CliMessages.MESSAGES.unableToReadKeyring(keystoreFile, new Exception("")).getMessage());
}

@Override
protected MavenOptions getCapturedMavenOptions() throws Exception {
Mockito.verify(actionFactory).install(any(), mavenOptions.capture(), any());
Mockito.verify(actionFactory).install(any(), mavenOptions.capture(), any(), any());
return mavenOptions.getValue();
}

Expand Down
Loading

0 comments on commit d0bcc2b

Please sign in to comment.