diff --git a/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrCertificateParseHandler.java b/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrCertificateParseHandler.java index 5a16df4ddec..54299229ee4 100644 --- a/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrCertificateParseHandler.java +++ b/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrCertificateParseHandler.java @@ -67,7 +67,7 @@ public void execute(OperationContext context, ModelNode operation) throws Operat InstallationManager installationManager = imf.create(serverHome, mavenOptions); try (InputStream is = context.getAttachmentStream(CERT_FILE.resolveModelAttribute(context, operation).asInt())) { - TrustCertificate tc = installationManager.parseCA(is); + TrustCertificate tc = installationManager.parseCertificate(is); ModelNode entry = new ModelNode(); entry.get(CERT_KEY_ID).set(tc.getKeyID()); diff --git a/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrResourceDefinition.java b/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrResourceDefinition.java index 364ce63bd1f..8ba1e987c68 100644 --- a/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrResourceDefinition.java +++ b/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrResourceDefinition.java @@ -195,6 +195,9 @@ public void registerOperations(ManagementResourceRegistration resourceRegistrati InstMgrCertificateRemoveHandler certificateRemoveHandler = new InstMgrCertificateRemoveHandler(imService, imf); resourceRegistration.registerOperationHandler(InstMgrCertificateRemoveHandler.DEFINITION, certificateRemoveHandler); + + InstMgrUnacceptedCertificateHandler unacceptedcertificateHandler = new InstMgrUnacceptedCertificateHandler(imService, imf); + resourceRegistration.registerOperationHandler(InstMgrUnacceptedCertificateHandler.DEFINITION, unacceptedcertificateHandler); } @Override @@ -408,7 +411,7 @@ public void execute(OperationContext context, ModelNode operation) throws Operat InstallationManager installationManager = imf.create(serverHome, mavenOptions); ModelNode mCertificates = new ModelNode().addEmptyList(); - Collection trustedCertificates = installationManager.listCA(); + Collection trustedCertificates = installationManager.listTrustedCertificates(); for (TrustCertificate tc : trustedCertificates) { ModelNode entry = new ModelNode(); entry.get(InstMgrConstants.CERT_KEY_ID).set(tc.getKeyID()); diff --git a/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrUnacceptedCertificateHandler.java b/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrUnacceptedCertificateHandler.java new file mode 100644 index 00000000000..6701f251a02 --- /dev/null +++ b/installation-manager/src/main/java/org/wildfly/core/instmgr/InstMgrUnacceptedCertificateHandler.java @@ -0,0 +1,93 @@ +/* + * Copyright The WildFly Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.wildfly.core.instmgr; + +import static org.wildfly.core.instmgr.InstMgrConstants.CERT_FILE; + +import java.io.FileOutputStream; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.Collection; + +import org.jboss.as.controller.AttributeDefinition; +import org.jboss.as.controller.OperationContext; +import org.jboss.as.controller.OperationDefinition; +import org.jboss.as.controller.OperationFailedException; +import org.jboss.as.controller.OperationStepHandler; +import org.jboss.as.controller.SimpleAttributeDefinitionBuilder; +import org.jboss.as.controller.SimpleOperationDefinitionBuilder; +import org.jboss.as.controller.registry.OperationEntry; +import org.jboss.dmr.ModelNode; +import org.jboss.dmr.ModelType; +import org.wildfly.installationmanager.MavenOptions; +import org.wildfly.installationmanager.spi.InstallationManager; +import org.wildfly.installationmanager.spi.InstallationManagerFactory; +import org.xnio.streams.Streams; + +/** + * Operation handler to get the history of the installation manager changes, either artifacts or configuration metadata as + * channel changes. + */ +public class InstMgrUnacceptedCertificateHandler extends InstMgrOperationStepHandler { + public static final String OPERATION_NAME = "unaccepted-certificates"; + + protected static final AttributeDefinition OFFLINE = SimpleAttributeDefinitionBuilder.create(InstMgrConstants.OFFLINE, ModelType.BOOLEAN) + .setStorageRuntime() + .setDefaultValue(ModelNode.FALSE) + .setRequired(false) + .build(); + + public static final OperationDefinition DEFINITION = new SimpleOperationDefinitionBuilder(OPERATION_NAME, InstMgrResolver.RESOLVER) + .withFlags(OperationEntry.Flag.HOST_CONTROLLER_ONLY) + .setReplyType(ModelType.OBJECT) + .setRuntimeOnly() + .setReplyValueType(ModelType.OBJECT) + .addParameter(OFFLINE) + .build(); + + InstMgrUnacceptedCertificateHandler(InstMgrService imService, InstallationManagerFactory imf) { + super(imService, imf); + } + + @Override + public void execute(OperationContext context, ModelNode operation) throws OperationFailedException { + context.addStep(new OperationStepHandler() { + @Override + public void execute(OperationContext context, ModelNode operation) throws OperationFailedException { + try { + final Path serverHome = imService.getHomeDir(); + final Path controllerTempDir = imService.getControllerTempDir(); + final boolean offline = OFFLINE.resolveModelAttribute(context, operation).asBoolean(); + final MavenOptions mavenOptions = new MavenOptions(null, offline); + final InstallationManager installationManager = imf.create(serverHome, mavenOptions); + + final Collection downloadedCerts = installationManager.downloadRequiredCertificates(); + + final ModelNode mCertificates = new ModelNode().addEmptyList(); + int i=0; + for (InputStream is : downloadedCerts) { + final Path certFile = controllerTempDir.resolve("required-cert-" + i++ + ".crt"); + try (FileOutputStream fos = new FileOutputStream(certFile.toFile())) { + Streams.copyStream(is, fos); + is.close(); + } + + final ModelNode entry = new ModelNode(); + entry.get(CERT_FILE).set(certFile.toAbsolutePath().toString()); + mCertificates.add(entry); + } + + final ModelNode result = context.getResult(); + result.set(mCertificates); + } catch (OperationFailedException | RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }, OperationContext.Stage.RUNTIME); + } +} diff --git a/installation-manager/src/main/java/org/wildfly/core/instmgr/cli/AddCertificatesCommand.java b/installation-manager/src/main/java/org/wildfly/core/instmgr/cli/AddCertificatesCommand.java index 12879dd2e45..1ff6746259f 100644 --- a/installation-manager/src/main/java/org/wildfly/core/instmgr/cli/AddCertificatesCommand.java +++ b/installation-manager/src/main/java/org/wildfly/core/instmgr/cli/AddCertificatesCommand.java @@ -34,6 +34,14 @@ public class AddCertificatesCommand extends AbstractInstMgrCommand { @Option(name = "non-interactive") private boolean nonInteractive; + public AddCertificatesCommand() { + } + + public AddCertificatesCommand(File certFile, boolean nonInteractive) { + this.certFile = certFile; + this.nonInteractive = nonInteractive; + } + @Override protected Operation buildOperation() { final ModelNode op = new ModelNode(); diff --git a/installation-manager/src/main/java/org/wildfly/core/instmgr/cli/UpdateCommand.java b/installation-manager/src/main/java/org/wildfly/core/instmgr/cli/UpdateCommand.java index 52ebf8993db..9c916b6eb6b 100644 --- a/installation-manager/src/main/java/org/wildfly/core/instmgr/cli/UpdateCommand.java +++ b/installation-manager/src/main/java/org/wildfly/core/instmgr/cli/UpdateCommand.java @@ -5,10 +5,18 @@ package org.wildfly.core.instmgr.cli; +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP; +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.RESULT; +import static org.wildfly.core.instmgr.InstMgrConstants.CERT_FILE; +import static org.wildfly.core.instmgr.InstMgrConstants.OFFLINE; + import java.io.File; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Collection; +import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; import org.aesh.command.CommandDefinition; import org.aesh.command.CommandException; @@ -23,9 +31,12 @@ import org.jboss.as.cli.operation.ParsedCommandLine; import org.jboss.as.controller.client.ModelControllerClient; import org.jboss.as.controller.client.Operation; +import org.jboss.as.controller.client.OperationBuilder; import org.jboss.dmr.ModelNode; import org.wildfly.core.cli.command.aesh.CLICommandInvocation; +import org.wildfly.core.instmgr.InstMgrCertificateParseHandler; import org.wildfly.core.instmgr.InstMgrConstants; +import org.wildfly.core.instmgr.InstMgrUnacceptedCertificateHandler; @CommandDefinition(name = "update", description = "Apply the latest available patches on a server instance.", activator = InstMgrActivator.class) public class UpdateCommand extends AbstractInstMgrCommand { @@ -68,6 +79,13 @@ public CommandResult execute(CLICommandInvocation commandInvocation) throws Comm final Boolean optNoResolveLocalCache = cmdParser.hasProperty("--" + NO_RESOLVE_LOCAL_CACHE_OPTION) ? noResolveLocalCache : null; final Boolean optUseDefaultLocalCache = cmdParser.hasProperty("--" + USE_DEFAULT_LOCAL_CACHE_OPTION) ? useDefaultLocalCache : null; + // call the download handler + Collection pendingCertificates = getPendingCertificates(ctx); + // call the import handler + if (!importPendingCertificates(pendingCertificates, ctx, commandInvocation)) { + return CommandResult.SUCCESS; + } + ListUpdatesAction.Builder listUpdatesCmdBuilder = new ListUpdatesAction.Builder() .setNoResolveLocalCache(optNoResolveLocalCache) .setUseDefaultLocalCache(optUseDefaultLocalCache) @@ -146,6 +164,65 @@ public CommandResult execute(CLICommandInvocation commandInvocation) throws Comm return CommandResult.SUCCESS; } + private boolean importPendingCertificates(Collection pendingCertificates, CommandContext ctx, CLICommandInvocation commandInvocation) throws CommandException, InterruptedException { + commandInvocation.println("The update is configured to verify the integrity of updated components, but following certificates need to be trusted:"); + + for (Path pendingCertificate : pendingCertificates) { + final ModelNode modelNode = this.executeOp(buildParseOperation(pendingCertificate), ctx, this.host).get(RESULT); + + commandInvocation.println("key-id: " + modelNode.get(InstMgrConstants.CERT_KEY_ID)); + commandInvocation.println("fingerprint: " + modelNode.get(InstMgrConstants.CERT_FINGERPRINT)); + commandInvocation.println("description: " + modelNode.get(InstMgrConstants.CERT_DESCRIPTION)); + commandInvocation.println(""); + + } + final String input = commandInvocation.inputLine(new Prompt("Import these certificates y/N ")); + if ("y".equals(input)) { + commandInvocation.print("Importing a trusted certificate"); + + for (Path pendingCertificate : pendingCertificates) { + new AddCertificatesCommand(pendingCertificate.toFile(), false).executeOp(ctx, this.host); + } + + return true; + } else { + commandInvocation.print("Importing canceled."); + return false; + } + } + + protected Operation buildParseOperation(Path pendingCertificate) { + final ModelNode op = new ModelNode(); + final OperationBuilder operationBuilder = OperationBuilder.create(op); + + op.get(OP).set(InstMgrCertificateParseHandler.DEFINITION.getName()); + op.get(CERT_FILE).set(0); + operationBuilder.addFileAsAttachment(pendingCertificate.toFile()); + + return operationBuilder.build(); + } + + private Collection getPendingCertificates(CommandContext ctx) throws CommandException { + if (confirm || dryRun) { + // skip the check in non-interactive runs because the certificate cannot be accepted either way + // the update will fail if certificate is required and will print error message + return Collections.emptyList(); + } + + final ModelNode op = new ModelNode(); + + op.get(OP).set(InstMgrUnacceptedCertificateHandler.DEFINITION.getName()); + op.get(OFFLINE).set(offline); + + final ModelNode modelNode = executeOp(OperationBuilder.create(op).build(), ctx, this.host); + + final List paths = modelNode.get(RESULT).asListOrEmpty(); + return paths.stream() + .map(n->n.get(CERT_FILE).asString()) + .map(Path::of) + .collect(Collectors.toList()); + } + private void printListUpdatesResult(CLICommandInvocation commandInvocation, List changesMn) { if (changesMn.isEmpty()) { commandInvocation.println("No updates found"); diff --git a/installation-manager/src/main/resources/org/wildfly/core/instmgr/LocalDescriptions.properties b/installation-manager/src/main/resources/org/wildfly/core/instmgr/LocalDescriptions.properties index 3ada3c9f96b..142353efdbf 100644 --- a/installation-manager/src/main/resources/org/wildfly/core/instmgr/LocalDescriptions.properties +++ b/installation-manager/src/main/resources/org/wildfly/core/instmgr/LocalDescriptions.properties @@ -91,3 +91,5 @@ installation-manager.certificate-remove=Remove a certificate to verify installat installation-manager.certificate-remove.key-id=The ID of a certificate to be removed from the list of certificates trusted to verify updated components. installation-manager.certificates=Certificates used to verify components. installation-manager.certificates.certificate.key-id=The ID of a certificate used to verify components. +installation-manager.unaccepted-certificates=Downloads certificates listed in the server channels, but not trusted yet. +installation-manager.unaccepted-certificates.offline=Only resolve certificates available locally. diff --git a/installation-manager/src/test/java/org/wildfly/core/instmgr/InstMgrResourceTestCase.java b/installation-manager/src/test/java/org/wildfly/core/instmgr/InstMgrResourceTestCase.java index ab4a3738bb0..e68df71e459 100644 --- a/installation-manager/src/test/java/org/wildfly/core/instmgr/InstMgrResourceTestCase.java +++ b/installation-manager/src/test/java/org/wildfly/core/instmgr/InstMgrResourceTestCase.java @@ -11,10 +11,13 @@ import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.INCLUDE_RUNTIME; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.NAME; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OPERATIONS; +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OUTCOME; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.READ_RESOURCE_DESCRIPTION_OPERATION; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.READ_RESOURCE_OPERATION; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.RECURSIVE; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.RESPONSE_HEADERS; +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.RESULT; +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SUCCESS; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.VALUE; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.WRITE_ATTRIBUTE_OPERATION; import static org.wildfly.core.instmgr.InstMgrConstants.CERT_DESCRIPTION; @@ -1545,6 +1548,21 @@ public void removeNonExistentCustomPatch() throws IOException { ); } + @Test + public void downloadUnacceptedCertificates() throws IOException { + TestInstallationManager.initialize(); + + PathAddress pathElements = PathAddress.pathAddress(CORE_SERVICE, InstMgrConstants.TOOL_NAME); + ModelNode op = Util.createEmptyOperation(InstMgrUnacceptedCertificateHandler.OPERATION_NAME, pathElements); + + ModelNode rsp = getController().execute(op, null, null, null); + Assert.assertEquals(SUCCESS, rsp.get(OUTCOME).asString()); + + final String certText = Files.readString(Path.of(rsp.get(RESULT).asList().get(0).get(CERT_FILE).asString())); + + Assert.assertEquals("test cert", certText); + } + /** * Creates and upload a custom patch associated to the customPatchManifest passed as argument. * It will use as Zip content the content available in the "test-repo-one" resource directory. diff --git a/testsuite/shared/src/main/java/org/wildfly/test/installationmanager/TestInstallationManager.java b/testsuite/shared/src/main/java/org/wildfly/test/installationmanager/TestInstallationManager.java index ae371a20e9e..1839a0616f1 100644 --- a/testsuite/shared/src/main/java/org/wildfly/test/installationmanager/TestInstallationManager.java +++ b/testsuite/shared/src/main/java/org/wildfly/test/installationmanager/TestInstallationManager.java @@ -6,14 +6,15 @@ package org.wildfly.test.installationmanager; import java.io.BufferedReader; +import java.io.ByteArrayInputStream; import java.io.FileInputStream; import java.io.FileOutputStream; -import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.MalformedURLException; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.time.Instant; @@ -302,7 +303,7 @@ public Collection verifyCandidate(Path candidatePath, CandidateTyp @Override public void acceptTrustedCertificates(InputStream certificate) throws Exception { - final TrustCertificate trustCertificate = parseCA(certificate); + final TrustCertificate trustCertificate = parseCertificate(certificate); lstTrustCertificates.add(trustCertificate); } @@ -317,12 +318,12 @@ public void revokeTrustedCertificate(String keyID) throws Exception { } @Override - public Collection listCA() throws Exception { + public Collection listTrustedCertificates() throws Exception { return Collections.unmodifiableCollection(lstTrustCertificates); } @Override - public TrustCertificate parseCA(InputStream certificate) throws Exception { + public TrustCertificate parseCertificate(InputStream certificate) throws Exception { String keyId = null; String fingerprint = null; String description = null; @@ -348,6 +349,13 @@ public TrustCertificate parseCA(InputStream certificate) throws Exception { return new TrustCertificate(keyId, fingerprint, description, "TRUSTED"); } + @Override + public Collection downloadRequiredCertificates() throws Exception { + final String cert = "test cert"; + final ByteArrayInputStream bais = new ByteArrayInputStream(cert.getBytes(StandardCharsets.UTF_8)); + return List.of(bais); + } + public static void zipDir(Path inputFile, Path target) throws IOException { try (FileOutputStream fos = new FileOutputStream(target.toFile()); ZipOutputStream zos = new ZipOutputStream(fos)) { ZipEntry entry = new ZipEntry(inputFile.getFileName().toString());