[java-identity-provider] 03/06: IDP-1595 Install Remote and From Fle

Rod Widdowson rdw at steadingsoftware.com
Tue Aug 4 13:06:43 UTC 2020


This is an automated email from the git hooks/post-receive script.

rdw pushed a commit to branch master
in repository java-identity-provider.

View the commit online:
http://git.shibboleth.net/view/?p=java-identity-provider.git;a=commit;h=fe9a88c4bd4f8342299aae9c0c0d32e617e2c76c

commit fe9a88c4bd4f8342299aae9c0c0d32e617e2c76c
Author: Rod Widdowson <rdw at steadingsoftware.com>
AuthorDate: Mon Aug 3 17:32:48 2020 +0100

    IDP-1595 Install Remote and From Fle
    
    https://issues.shibboleth.net/jira/browse/IDP-1595
    
    Plus tests.
    
    NOTA: The "accept cert" callback is still pending.
---
 .../installer/plugin/PluginInstallerArguments.java |  82 +++++++++
 .../idp/installer/plugin/PluginInstallerCLI.java   |  33 +++-
 .../idp/installer/plugin/impl/PluginInstaller.java |  95 ++++++-----
 .../idp/installer/plugin/PluginCLITest.java        |  82 ++++++++-
 .../src/test/resources/credentials/truststore.asc  | 189 +++++++++++++++++++++
 5 files changed, 427 insertions(+), 54 deletions(-)

diff --git a/idp-installer/src/main/java/net/shibboleth/idp/installer/plugin/PluginInstallerArguments.java b/idp-installer/src/main/java/net/shibboleth/idp/installer/plugin/PluginInstallerArguments.java
index 41a07228f..732d2d659 100644
--- a/idp-installer/src/main/java/net/shibboleth/idp/installer/plugin/PluginInstallerArguments.java
+++ b/idp-installer/src/main/java/net/shibboleth/idp/installer/plugin/PluginInstallerArguments.java
@@ -17,10 +17,17 @@
 
 package net.shibboleth.idp.installer.plugin;
 
+import java.io.File;
 import java.io.PrintStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.file.Path;
 
 import javax.annotation.Nullable;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import com.beust.jcommander.Parameter;
 
 import net.shibboleth.ext.spring.cli.AbstractCommandLineArguments;
@@ -30,6 +37,9 @@ import net.shibboleth.ext.spring.cli.AbstractCommandLineArguments;
  */
 public class PluginInstallerArguments extends AbstractCommandLineArguments {
 
+    /** Logger. */
+    private final Logger log = LoggerFactory.getLogger(PluginInstallerArguments.class);
+
     /** The PluginId - usually used to drive the update. */
     @Parameter(names= {"-p", "--pluginId"})
     @Nullable private String pluginId;
@@ -42,6 +52,19 @@ public class PluginInstallerArguments extends AbstractCommandLineArguments {
     @Parameter(names= {"-fl", "--full-list"})
     @Nullable private boolean fullList;
 
+    /** What to install. */
+    @Parameter(names= {"-i", "--input"})
+    @Nullable private String input;
+
+    /** Decomposed input - name. */
+    @Nullable private String inputName;
+
+    /** Decomposed input - directory . */
+    @Nullable private Path inputDirectory;
+
+    /** Decomposed input - base URL. */
+    @Nullable private URL inputURL;
+
     /** Operation enum. */
     public enum OperationType {
         /** Update a known install. */
@@ -66,6 +89,27 @@ public class PluginInstallerArguments extends AbstractCommandLineArguments {
         return pluginId;
     }
 
+    /** Get the digested parent URL.
+     * @return Returns the digested parent URL.
+     */
+    public URL getInputURL() {
+        return inputURL;
+    }
+
+    /** Get the file Name.
+     * @return Returns the digested file Name.
+     */
+    public String getInputFileName() {
+        return inputName;
+    }
+
+    /** Get the digested input directory.
+     * @return Returns the digested input directory.
+     */
+    public Path getInputDirectory() {
+        return inputDirectory;
+    }
+
     /** Are we doing a full List?
      * @return {@link #fullList}
      */
@@ -98,7 +142,44 @@ public class PluginInstallerArguments extends AbstractCommandLineArguments {
         }
         if (list || fullList) {
             operation = OperationType.LIST;
+            if (input !=  null) {
+                log.error("Cannot List and Install in the same operation.");
+                throw new IllegalArgumentException("Cannot List and Install in the same operation.");
+            }
+            return;
+        }
+        if (input != null) {
+            operation = decodeInput() ;
+        }
+    }
+
+    /** Given an inout string, work out what the parts are.
+     * @return Whether this is a remote install or a local one.
+     */
+    private OperationType decodeInput() {
+        try {
+            final URL inputAsURL = new URL(input);
+            if ("https".equals(inputAsURL.getProtocol()) || "http".equals(inputAsURL.getProtocol())) {
+                final int i = input.lastIndexOf('/')+1;
+                inputURL = new URL(input.substring(0, i));
+                inputName = input.substring(i);
+                log.trace("Found URL: {}\t{}", inputDirectory, inputName);
+                return OperationType.INSTALLREMOTE;
+            }
+        } catch (final MalformedURLException e) {
+            log.trace("urg");
+        }
+        // Must be a file
+        final File inputAsFile = new File(input);
+        if (!inputAsFile.exists()) {
+            log.error("File {} does not exist", inputAsFile.getAbsolutePath());
+            throw new IllegalArgumentException("Input File does not exist");
         }
+        final Path inputAsPath = Path.of(inputAsFile.getAbsolutePath());
+        inputDirectory = inputAsPath.getParent();
+        inputName = inputAsPath.getFileName().toString();
+        log.trace("Found File: {}\t{}", inputDirectory, inputName);
+        return OperationType.INSTALLDIR;
     }
 
     /** {@inheritDoc} */
@@ -113,6 +194,7 @@ public class PluginInstallerArguments extends AbstractCommandLineArguments {
         out.println();
         out.println(String.format("  %-22s %s", "-l, --list", "Brief Information of all installed plugins"));
         out.println(String.format("  %-22s %s", "-fl, --full-list", "Full details of all installed plugins"));
+        out.println(String.format("  %-22s %s", "-i, --input <what>", "Install (file name or web address)"));
         out.println();
     }
 
diff --git a/idp-installer/src/main/java/net/shibboleth/idp/installer/plugin/PluginInstallerCLI.java b/idp-installer/src/main/java/net/shibboleth/idp/installer/plugin/PluginInstallerCLI.java
index 5f1e50fd8..197b279ca 100644
--- a/idp-installer/src/main/java/net/shibboleth/idp/installer/plugin/PluginInstallerCLI.java
+++ b/idp-installer/src/main/java/net/shibboleth/idp/installer/plugin/PluginInstallerCLI.java
@@ -19,6 +19,7 @@ package net.shibboleth.idp.installer.plugin;
 
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.security.Security;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
@@ -28,6 +29,8 @@ import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
 import org.apache.http.client.HttpClient;
+import org.apache.tools.ant.BuildException;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.BeansException;
@@ -121,11 +124,15 @@ public final class PluginInstallerCLI extends AbstractCommandLine<PluginInstalle
     }
     
     /** {@inheritDoc} */
+    //CheckStyle: CyclomaticComplexity OFF
     protected int doRun(final PluginInstallerArguments args) {
         final int ret = super.doRun(args);
         if (ret != RC_OK) {
             return ret;
         }
+        if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
+            Security.addProvider(new BouncyCastleProvider());
+        }
         final Set<Entry<String, HttpClient>> clients =
                 getApplicationContext().getBeansOfType(HttpClient.class).entrySet();
         if (clients.isEmpty()) {
@@ -147,17 +154,35 @@ public final class PluginInstallerCLI extends AbstractCommandLine<PluginInstalle
                     doList(args.getFullList(), args.getPluginId());
                     break;
 
+                case INSTALLDIR:
+                    if (args.getPluginId() != null) {
+                        installer.setPluginId(args.getPluginId());
+                    }
+                    installer.installPlugin(args.getInputDirectory(), args.getInputFileName());
+                    break;
+
+                case INSTALLREMOTE:
+                    if (args.getPluginId() != null) {
+                        installer.setPluginId(args.getPluginId());
+                    }
+                    installer.installPlugin(args.getInputURL(), args.getInputFileName());
+                    break;
+
                 default:
                     getLogger().error("Invalid operation");
-                    return RC_IO;
+                    return RC_INIT;
             }
 
-        } catch (final ComponentInitializationException | BeansException e) {
-            getLogger().error("Plugin failed", e);
+        } catch (final BeansException e) {
+            getLogger().error("Plugin Install failed", e);
+            return RC_INIT;
+        } catch (final ComponentInitializationException | BuildException e) {
+            getLogger().error("Plugin Install failed:", e);
             return RC_IO;
-        } 
+        }
         return ret;
     }
+    //CheckStyle: CyclomaticComplexity OM
 
     /** Build the installer.
      * @throws ComponentInitializationException as required*/
diff --git a/idp-installer/src/main/java/net/shibboleth/idp/installer/plugin/impl/PluginInstaller.java b/idp-installer/src/main/java/net/shibboleth/idp/installer/plugin/impl/PluginInstaller.java
index f5475e6ba..f42befe84 100644
--- a/idp-installer/src/main/java/net/shibboleth/idp/installer/plugin/impl/PluginInstaller.java
+++ b/idp-installer/src/main/java/net/shibboleth/idp/installer/plugin/impl/PluginInstaller.java
@@ -84,7 +84,8 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
     private static List<String> disallowedPaths = List.of("dist", "system", "webapp");
 
     /** Class logger. */
-    @Nonnull private final Logger log = LoggerFactory.getLogger(PluginInstaller.class);
+    @Nonnull
+    private static final Logger LOG = LoggerFactory.getLogger(PluginInstaller.class);
 
     /** Where we are installing to. */
     @NonnullAfterInit private Path idpHome;
@@ -171,11 +172,11 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
     public void installPlugin(@Nonnull final Path base,
                               @Nonnull @NotEmpty final String fileName) throws BuildException {
         if (!Files.exists(base.resolve(fileName))) {
-            log.error("Could not find distribution {}", base.resolve(fileName));
+            LOG.error("Could not find distribution {}", base.resolve(fileName));
             throw new BuildException("Could not find distribution");
         }
         if (!Files.exists(base.resolve(fileName + ".asc"))) {
-            log.error("Could not find distribution {}", base.resolve(fileName + ".asc"));
+            LOG.error("Could not find distribution {}", base.resolve(fileName + ".asc"));
             throw new BuildException("Could not find signature for distribution");
         }
 
@@ -183,15 +184,15 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
         setupPluginId();
         checkSignature(base, fileName);
         getDescription();
-        log.info("Installing Plugin {} version {}.{}.{}", pluginId,
+        LOG.info("Installing Plugin {} version {}.{}.{}", pluginId,
                 description.getMajorVersion(),description.getMinorVersion(), description.getPatchVersion());
 
         if (!description.getAdditionalPropertyFiles().isEmpty()) {
-            log.error("Additional property files not supported");
+            LOG.error("Additional property files not supported");
             throw new BuildException("Uninstallable plugin");
         }
         if (!description.getPropertyMerges().isEmpty()) {
-            log.error("Prroperty merges not supported");
+            LOG.error("Prroperty merges not supported");
             throw new BuildException("Uninstallable plugin");
         }
 
@@ -218,16 +219,16 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
             for (final Pair<URL, Path> pair : description.getExternalFilePathsToCopy()) {
                 final Path to = idpHome.resolve(pair.getSecond());
                 if (Files.exists(to)) {
-                    log.warn("{} exists, not copied", to);
+                    LOG.warn("{} exists, not copied", to);
                     continue;
                 }
                 if (!acceptDownload.test(new Pair<>(pair.getFirst(), to))) {
-                    log.info("Did not download {} to {}", pair.getFirst(), to);
+                    LOG.info("Did not download {} to {}", pair.getFirst(), to);
                     continue;
                 }
                 buildHttpClient();
                 createParent(to);
-                log.debug("Copying from {} to {}", pair.getFirst(), to);
+                LOG.debug("Copying from {} to {}", pair.getFirst(), to);
                 final Resource from  = new HTTPResource(httpClient, pair.getFirst());
                 try (final InputStream in = new BufferedInputStream(from.getInputStream());
                      final OutputStream out =  new BufferedOutputStream(new FileOutputStream(to.toFile()))) {
@@ -235,7 +236,7 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
                     in.transferTo(out);
 
                 } catch (final IOException e) {
-                    log.error("Could not copy from {} to {}",  from, to, e);
+                    LOG.error("Could not copy from {} to {}",  from, to, e);
                     throw new BuildException(e);
                 }
             }
@@ -259,18 +260,18 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
 
                final ServiceLoader<PluginDescription> plugins = ServiceLoader.load(PluginDescription.class, loader);
                for (final PluginDescription plugin:plugins) {
-                   log.debug("Found Service announcing itself as {}", plugin.getPluginId() );
+                   LOG.debug("Found Service announcing itself as {}", plugin.getPluginId() );
                    if (pluginId.equals(plugin.getPluginId())) {
                        description = plugin;
                        return;
                    }
-                   log.trace("Did not match {}", pluginId);
+                   LOG.trace("Did not match {}", pluginId);
                }
            }
-           log.error("Could not locate description for {} in distribution {}", pluginId, libDir);
+           LOG.error("Could not locate description for {} in distribution {}", pluginId, libDir);
            throw new BuildException("Could not locate PluginDescription");
         } catch (final IOException e) {
-            log.error("Could not get description of {} from {}", pluginId, libDir, e);
+            LOG.error("Could not get description of {} from {}", pluginId, libDir, e);
             throw new BuildException(e);
         }
     }
@@ -282,7 +283,7 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
         for (final Path p : description.getFilePathsToCopy()) {
             for (final String disallowedPath : disallowedPaths) {
                 if (p.startsWith(disallowedPath)) {
-                    log.error("Path {} contained disallowed location", p);
+                    LOG.error("Path {} contained disallowed location", p);
                     throw new BuildException("Copy to banned location");
                 }
             }
@@ -290,22 +291,22 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
             final Path from = distribution.resolve(p);
             final Path to = idpHome.resolve(p);
             if (Files.exists(to)) {
-                log.debug("File {} exists, skipping", to);
+                LOG.debug("File {} exists, skipping", to);
                 continue;
             }
             if (!Files.exists(from)) {
-                log.warn("Source File {} does not exists, skipping", from);
+                LOG.warn("Source File {} does not exists, skipping", from);
                 continue;
             }
             try {
                 createParent(to);
-                log.debug("Copying from {} to {}", from, to);
+                LOG.debug("Copying from {} to {}", from, to);
                 try (final InputStream in = new BufferedInputStream(new FileInputStream(from.toFile()));
                      final OutputStream out =  new BufferedOutputStream(new FileOutputStream(to.toFile()))) {
                     in.transferTo(out);
                 }
             } catch (final IOException e) {
-                log.error("Could not copy from {} to {}",  from, to, e);
+                LOG.error("Could not copy from {} to {}",  from, to, e);
                 throw new BuildException(e);
             }
         }
@@ -319,13 +320,13 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
     private void createParent(final Path file) throws IOException, BuildException {
         final Path parent = file.resolve("..");
         if (!Files.exists(parent)) {
-            log.debug("Creating parent directory {}", parent);
+            LOG.debug("Creating parent directory {}", parent);
             Files.createDirectories(parent);
         } else if (!Files.isDirectory(parent)) {
-            log.error("{} exists and is not a directory", parent);
+            LOG.error("{} exists and is not a directory", parent);
             throw new BuildException("Parent of target file was not a directory");
         } else {
-            log.trace("Parent directory {} existed", parent);
+            LOG.trace("Parent directory {} existed", parent);
         }  
     }
 
@@ -336,7 +337,7 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
      */
     private void installWebapp(final Path myWebApp) throws BuildException {
         final Path from = distribution.resolve("edit-webapp");
-        log.debug("Copying distribution from {} to {}", from, myWebApp);
+        LOG.debug("Copying distribution from {} to {}", from, myWebApp);
         final Copy copy = InstallerSupport.getCopyTask(from, myWebApp);
         copy.execute();
     }
@@ -354,7 +355,7 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
             download(baseResource, fileName);
             download(baseResource, fileName + ".asc");
         } catch (final IOException e) {
-            log.error("Error in download", e);
+            LOG.error("Error in download", e);
             throw new BuildException(e);
         }
     }
@@ -362,11 +363,11 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
     /** Build the Http Client if it doesn't exist. */
     private void buildHttpClient() {
         if (httpClient == null) {
-            log.debug("No HttpClient built, creating default");
+            LOG.debug("No HttpClient built, creating default");
             try {
                 httpClient = new HttpClientBuilder().buildClient();
             } catch (final Exception e) {
-                log.error("Could not create HttpClient", e);
+                LOG.error("Could not create HttpClient", e);
                 throw new BuildException(e);
             }
         }
@@ -380,7 +381,7 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
     private void download(final Resource baseResource, final String fileName) throws IOException {
         final Resource fileResource = baseResource.createRelativeResource(fileName);
         final Path filePath = downloadDirectory.resolve(fileName);
-        log.debug("Downloading from {} to {}", fileResource.getDescription(), filePath);
+        LOG.debug("Downloading from {} to {}", fileResource.getDescription(), filePath);
         try (final OutputStream fileOut = new BufferedOutputStream(
                 new FileOutputStream(filePath.toFile()))) {
             fileResource.getInputStream().transferTo(fileOut);
@@ -404,20 +405,20 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
                 ArchiveEntry entry = null;
                 while ((entry = inStream.getNextEntry()) != null) {
                     if (!inStream.canReadEntryData(entry)) {
-                        log.warn("Could not read next entry from {}", inStream);
+                        LOG.warn("Could not read next entry from {}", inStream);
                         continue;
                     }
                     final File output = unpackDirectory.resolve(entry.getName()).toFile();
-                    log.trace("Unpacking {} to {}", entry.getName(), output);
+                    LOG.trace("Unpacking {} to {}", entry.getName(), output);
                     if (entry.isDirectory()) {
                         if (!output.isDirectory() && !output.mkdirs()) {
-                            log.error("Failed to create directory {}", output);
+                            LOG.error("Failed to create directory {}", output);
                             throw new BuildException("failed to create unpacked directory");
                         }
                     } else {
                         final File parent = output.getParentFile();
                         if (!parent.isDirectory() && !parent.mkdirs()) {
-                            log.error("Failed to create parent directory {}", parent);
+                            LOG.error("Failed to create parent directory {}", parent);
                             throw new BuildException("failed to create unpacked directory");
                         }
                         try (OutputStream outStream = Files.newOutputStream(output.toPath())) {
@@ -429,12 +430,12 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
             try (final DirectoryStream<Path> unpackDirStream = Files.newDirectoryStream(unpackDirectory)) {
                 final Iterator<Path> contents = unpackDirStream.iterator();
                 if (!contents.hasNext()) {
-                    log.error("No contents unpacked from {}", fullName);
+                    LOG.error("No contents unpacked from {}", fullName);
                     throw new BuildException("Distro was empty");
                 }
                 distribution = contents.next();
                 if (contents.hasNext()) {
-                    log.error("Too many packages in distributions {}", fullName);
+                    LOG.error("Too many packages in distributions {}", fullName);
                     throw new BuildException("Too many packages in distributions");
                 }
             }
@@ -451,14 +452,14 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
      */
     private boolean isZip(final String fileName) throws BuildException {
         if (fileName.length() <= 7) {
-            log.error("Improbably small file name: {}", fileName);
+            LOG.error("Improbably small file name: {}", fileName);
             throw new BuildException("Improbably small file name");
         }
         if (".zip".equalsIgnoreCase(fileName.substring(fileName.length()-4))) {
             return true;
         }
         if (!".tar.gz".equalsIgnoreCase(fileName.substring(fileName.length()-7))) {
-            log.warn("FileName {} did not end with .zip or .tar.gz, assuming tar-gz", fileName);
+            LOG.warn("FileName {} did not end with .zip or .tar.gz, assuming tar-gz", fileName);
         }
         return false;
     }
@@ -483,7 +484,7 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
     private void setupPluginId() throws BuildException {
         final File propertyFile = distribution.resolve("bootstrap").resolve("id.property").toFile();
         if (!propertyFile.exists()) {
-            log.error("Could not locate identity of plugin at {}", propertyFile);
+            LOG.error("Could not locate identity of plugin at {}", propertyFile);
             throw new BuildException("Could not locate identity of plugin");
         }
         try (final InputStream inStream = new BufferedInputStream(new FileInputStream(propertyFile))) {
@@ -491,16 +492,16 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
             idProperties.load(inStream);
             final String id = StringSupport.trimOrNull(idProperties.getProperty("pluginid"));
             if (id == null) {
-                log.error("identity property file {} did not contain 'pluginid' property", propertyFile);
+                LOG.error("identity property file {} did not contain 'pluginid' property", propertyFile);
                 throw new BuildException("No property in ID file");
             }
             if (pluginId != null && !pluginId.equals(id)) {
-                log.error("Downloaded plugin id {} overriden by provided id {}", id, pluginId);
+                LOG.error("Downloaded plugin id {} overriden by provided id {}", id, pluginId);
             } else {
                 setPluginId(id);
             }
         } catch (final IOException e) {
-            log.error("Could not load plugin identity at {}", propertyFile, e);
+            LOG.error("Could not load plugin identity at {}", propertyFile, e);
             throw new BuildException(e);
         }
     }
@@ -519,10 +520,10 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
             trust.initialize();
             final Signature sig = TrustStore.signatureOf(sigStream);
             if (!trust.contains(sig)) {
-                log.info("TrustStore does not contain signature {}", sig);
+                LOG.info("TrustStore does not contain signature {}", sig);
                 final File certs = distribution.resolve("bootstrap").resolve("keys.txt").toFile();
                 if (!certs.exists()) {
-                    log.info("No embedded keys file, signature check fails");
+                    LOG.info("No embedded keys file, signature check fails");
                     throw new BuildException("No Certificate found to check signiture o distribution");
                 }
                 try (final InputStream keysStream = new BufferedInputStream(
@@ -530,7 +531,7 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
                     trust.importCertificateFromStream(sig, keysStream, acceptCert);
                 }
                 if (!trust.contains(sig)) {
-                    log.info("Certificate not added to Trust Store");
+                    LOG.info("Certificate not added to Trust Store");
                     throw new BuildException("Could not check signature of distribution");
                 }
             }
@@ -538,13 +539,13 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
             try (final InputStream distroStream = new BufferedInputStream(
                 new FileInputStream(base.resolve(fileName).toFile()))) {
                 if (!trust.checkSignature(distroStream, sig)) {
-                    log.info("Signature checked for {} failed", fileName);
+                    LOG.info("Signature checked for {} failed", fileName);
                     throw new BuildException("Signature check failed");
                 }
             }
 
         } catch (final ComponentInitializationException | IOException e) {
-            log.error("Could not manage truststore for [{}, {}] ", idpHome, pluginId, e);
+            LOG.error("Could not manage truststore for [{}, {}] ", idpHome, pluginId, e);
             throw new BuildException(e);
         }
     }
@@ -582,11 +583,11 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
     /** Delete a directory tree. 
      * @param directory what to delete
      */
-    private void deleteTree(@Nullable final Path directory) {
+    public static void deleteTree(@Nullable final Path directory) {
         if (directory == null || !Files.exists(directory)) {
             return;
         }
-        log.debug("Deleting directory {}", directory);
+        LOG.debug("Deleting directory {}", directory);
         try {
             Files.walkFileTree(directory, new SimpleFileVisitor<Path>() {
                 @Override 
@@ -604,7 +605,7 @@ public final class PluginInstaller extends AbstractInitializableComponent implem
                 }
             });
         } catch (final IOException e) {
-            log.error("Couldn't delete {}", directory, e);
+            LOG.error("Couldn't delete {}", directory, e);
         }
     }
     
diff --git a/idp-installer/src/test/java/net/shibboleth/idp/installer/plugin/PluginCLITest.java b/idp-installer/src/test/java/net/shibboleth/idp/installer/plugin/PluginCLITest.java
index f2d87ff99..5b8b1ffe8 100644
--- a/idp-installer/src/test/java/net/shibboleth/idp/installer/plugin/PluginCLITest.java
+++ b/idp-installer/src/test/java/net/shibboleth/idp/installer/plugin/PluginCLITest.java
@@ -18,34 +18,110 @@
 package net.shibboleth.idp.installer.plugin;
 
 import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.fail;
 
 import java.io.BufferedOutputStream;
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
 
+import org.apache.http.client.HttpClient;
 import org.springframework.core.io.ClassPathResource;
 import org.springframework.core.io.Resource;
+import org.testng.annotations.BeforeSuite;
 import org.testng.annotations.Test;
 
 import net.shibboleth.ext.spring.cli.AbstractCommandLine;
+import net.shibboleth.ext.spring.resource.HTTPResource;
+import net.shibboleth.idp.installer.plugin.impl.PluginInstaller;
+import net.shibboleth.utilities.java.support.httpclient.HttpClientBuilder;
 
 @SuppressWarnings("javadoc")
 public class PluginCLITest extends BasePluginTest {
     
-    @Test(enabled = true) public void testList() throws IOException {
+    private final String RHINO_DISTRO = "https://build.shibboleth.net/nexus/service/local/repositories/releases/content/net/shibboleth/idp/plugin/scripting/idp-plugin-rhino-dist/0.1.0/idp-plugin-rhino-dist-0.1.0.tar.gz";
+
+    private File plugin;
+
+    @BeforeSuite public void setUp() throws IOException
+    {
         System.setProperty("net.shibboleth.idp.cli.idp.home",getIdpHome().toString());
         final Resource pluginInstaller = new ClassPathResource("conf/admin/plugin-installer.xml");
-        final File plugin = getIdpHome().resolve("conf").resolve("admin").resolve("plugin-installer.xml").toFile();
+        plugin = getIdpHome().resolve("conf").resolve("admin").resolve("plugin-installer.xml").toFile();
         plugin.createNewFile();
         
         try (final InputStream is = pluginInstaller.getInputStream();
              final BufferedOutputStream os = new BufferedOutputStream(new FileOutputStream(plugin))) {
             is.transferTo(os);
         }
-    
+    }
+
+    @Test(enabled = true) public void testList() throws IOException {
         assertEquals(PluginInstallerCLI.runMain(new String[] { plugin.getAbsolutePath(), "-fl"}),
                 AbstractCommandLine.RC_OK);
     }
+
+    @Test(enabled = true) public void testLocal() {
+        assertEquals(PluginInstallerCLI.runMain(new String[] { plugin.getAbsolutePath(), "-i", "a"}),
+                AbstractCommandLine.RC_INIT);
+    }
+
+    @Test(enabled = true, dependsOnMethods = {"testRhinoLocal"}) public void testRhinoWeb() {
+            assertEquals(PluginInstallerCLI.runMain(new String[] { plugin.getAbsolutePath(),
+                    "-i", RHINO_DISTRO,
+                    "-p", "net.shibboleth.idp.plugin.rhino"}),
+                    AbstractCommandLine.RC_OK);
+    }
+
+    @Test(enabled = true) public void testRhinoLocal() {
+        Path unpack = null;
+        try {
+            unpack = Files.createTempDirectory("rhinoLocal");
+            final HttpClient client = new HttpClientBuilder().buildClient();
+            Resource from = new HTTPResource(client, RHINO_DISTRO);
+            try (final InputStream in = from.getInputStream(); 
+                 final OutputStream out = new BufferedOutputStream(new FileOutputStream(unpack.resolve("rhino.tar.gz").toFile()))) {
+                    in.transferTo(out);
+            }
+            from = new HTTPResource(client, RHINO_DISTRO + ".asc");
+            try (final InputStream in = from.getInputStream(); 
+                    final OutputStream out = new BufferedOutputStream(new FileOutputStream(unpack.resolve("rhino.tar.gz.asc").toFile()))) {
+                       in.transferTo(out);
+               }
+            
+            assertEquals(PluginInstallerCLI.runMain(new String[] { plugin.getAbsolutePath(),
+                    "-p", "net.shibboleth.idp.plugin.rhino",
+                    "-i", unpack.resolve("rhino.tar.gz").toString()}),
+                    AbstractCommandLine.RC_IO);
+            //
+            // Populate the new key store
+            //
+            final Path trustStorePath = getIdpHome().
+                    resolve("credentials").
+                    resolve("net.shibboleth.idp.plugin.rhino").
+                    resolve("truststore.asc");
+            from = new ClassPathResource("credentials/truststore.asc");
+            try (final InputStream in = from.getInputStream(); 
+                    final OutputStream out = new BufferedOutputStream(new FileOutputStream(trustStorePath.toFile(), true))) {
+                in.transferTo(out);
+            }
+            //
+            // try again
+            //
+            assertEquals(PluginInstallerCLI.runMain(new String[] { plugin.getAbsolutePath(), 
+                    "-i", unpack.resolve("rhino.tar.gz").toString(),
+                    "-p", "net.shibboleth.idp.plugin.rhino"}),
+                    AbstractCommandLine.RC_OK);
+        } catch (Exception e) {
+            fail("Failed" + e);
+        } finally {
+            if (unpack != null) {
+                PluginInstaller.deleteTree(unpack);
+            }
+        }
+    }
 }
diff --git a/idp-installer/src/test/resources/credentials/truststore.asc b/idp-installer/src/test/resources/credentials/truststore.asc
new file mode 100644
index 000000000..0ccc32281
--- /dev/null
+++ b/idp-installer/src/test/resources/credentials/truststore.asc
@@ -0,0 +1,189 @@
+
+pub   4096R/2A4B3FF0 2019-09-18
+uid                  Rod Widdowson <rdw at steadingsoftware.com>
+sub   4096R/441D628D 2019-09-18
+
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQINBF2CHx4BEADfUvo4sPc8a8uQjfvoHdBY0qmgwXlcAOyllBKZ5g/wYKZO1Lkp
+LZh/dQFBK4AjqRnzs0dq0arK0W5WijOYjQ+s5cd1MMXmzqgXG02eAS4ooK6KsSwv
+mo2FydddQKFbwLkGdS/UXcENWNnzGeJhmjmcPSLgRo2hsSh63cFltq2+8fwl1fQ9
+FzZwscOOkJGBb7/nqdHdnvL9yrRameYFo2iWF9P52cjfv5NiNklkDBUHeISuX48I
+pI+kSOz2b7/aP4vKOKOpOaas0MAdcYT8AcwrCD9OhFFzfuIs/S9+rHGs/+M0vcWG
+DPR+IY0L7Stgkc2Hz1gazuqHBiOBq5VnDOE9nkZ/mY/HwMJzYCtuwQSPyidY7sRk
+lrD5NzXVXGtUri/vghOgRcT3PG8P6zL3UrJi+XgwNSmNHfWQR+wt2Rs9SqrHav+g
+xqHHxCmcH/7HSZEAFi0ooxybOCLeKuAuu94TWi/KAF6/d9iNLekXpuodKl/ceO6d
+9h8791Rjh9a2BR6+VkIxf2zSzb0IPrmGfCjq5Jhc7m3AzAYNWJs0e/FK6G3FYfIS
+TYAGEUJgiWkm7zpV8eDiUo7Qjs9YTQPuuVjtdVCzt3BNm5NUKyrssDxYFs6ryFop
+FDoFewGPhFTnh8wTo0PUYpVj6ZUC8YniFE+XAOq8hufgbiqMcFn+2A/qMQARAQAB
+tChSb2QgV2lkZG93c29uIDxyZHdAc3RlYWRpbmdzb2Z0d2FyZS5jb20+iQJOBBMB
+CAA4FiEESvTYPu3fQ9o8BssxAUg/JipLP/AFAl2CHx4CGwMFCwkIBwIGFQoJCAsC
+BBYCAwECHgECF4AACgkQAUg/JipLP/Cr6hAA0RQyvAvWXnVNA+js6aNpqNO+rGyw
+sm+ajSuPNCyrkELlR08qpTxaezQ3soDJ9iWYgpPV767szs0yZmbnEEq1QAJXYsq6
+0pGVtuEtTmqRYcxuZwwqfkGJhs8p2C7/U5IcbvrvlUpHD6G4CEaH/CHthOpyVtBV
+7cHqt1l0+6+928UTdkZl3OPrbQloHHgHN14LPWY2MiGCDIbLx5wOrwrJ8hoiGeK3
+npfUZsrothsh/hClMWB4jf5sM/fltr/dT+Vi09JjE7/2wDTIq1R7UsAUte6sfhb/
+GLTVdQmG8jsWfMWP3rKDFBRiXHNzM/gNP2mHnXLO6UlSkV2JuJ9fgSKiBpXhtrI3
+7PTNnJdZz1Lm6rl1T9jgWdzRkl4x17bBzgU8GkTsRBS2vuRFDdsoywPJJgw7sdP3
+FTVBFfCGil9DAzKjGtbeIM2UBfx/7ltqVrHMR5pgto7aXpAt7N1X4ocTL/BSlZCk
+nFXnMIpW+Vsg6NDg5bRyC3adaReL3APnMkmBSSiqu4hFwrD6MVXcLN9tQ70sW3QT
+e1lOiUMeGQkVhiRWiZLeQd9jIeN1hoDGBnBYBgAeawGO5fGAJCTosXLP07C8lFLF
+5SYN8pBx0acuZMVwG0NKGcYyP/3Z+3j4kWIlpN+x455nQs/n/ZBGLlkVygtlXCC5
+YXIbnFuzOi3Lofi5Ag0EXYIfHgEQAN1hvXOZMrBeiutbBj8l+aAb7MwAAofjiuU7
+winmi0sgIRMCwTDSgubPpcaPxBmKLSVplngJRSwnMcb1bQmx8lVRmSjEoD5Uui/c
+CQsQY8yd1rQQbPUlOWrlTMjesctVVryCb4jnVQO+vsotI73JGTI2RFHTpMbPv03R
+rk3arxenfwS86XAivDRR9NZP8VvysXJCgua3t+Vm4bZNOnqoEoWBEAn0d1mVxYz5
+PFcO3jP4S8ceyyhCoctcyCO3xdSWuwQWJbKCHSi9bByuex7lUbGaoWO37IMkTE4+
+7/rtlUA/NoFNzxnQVHo4FhTBj6KBOFxrL09VCk1B5kfP9jQ2/F+sWclhHmcFNTmR
+xpVepkSOuAisCDAZIMTYJiI4rPJSTDrhcy1DsSTTNFv7j0U5ISNd16HMyg4xUoru
+U6nu5VuSO/6F8yWRpZi08UxUSREvrAKIMfKMZd44DC3ObYVsEr5uO/jS7KL69PqP
+OmLJnVcL10eZmBAA6XGinqnDZmd3mR03aFRw3QKNGgDap7Pi+kN/WHLM+O9QElyx
+wgihXcw7/TIDajVqxuqGMZhz21cpJx5EcaZlCYQKFRtuzuo1L4fcabzSwd3fVw8l
+QZlouja0pEcb5dQueyvnm42tSZahAi5Rb8qeh0cWG1b0bvxe+X0vH5BYpk+iB8mz
+1eA3NX/JABEBAAGJAjYEGAEIACAWIQRK9Ng+7d9D2jwGyzEBSD8mKks/8AUCXYIf
+HgIbDAAKCRABSD8mKks/8EPQEADGWjxyxh9HoR7d3mTUjuurLjR/9cu4JQTHxZ2k
+Z1fcDua1IBeJhZRb9P2fSogDxEGeLpUNTnCxKHq5tlJUKYrtFBqab8CtvGEif5i0
+Nh1raWPw3IzqtGGu5QkRL7xebxDURPfO/vfoYUbNF+tMUNGDDUu5FObde0oxX2Cq
+8vjaSrlthcQpFT/4z4K3ecU+Orq2L9sUw27/FMgwC0DN1xIvi8no/wwZD557XgVJ
+WL6VYn1UQxz0h1zJiOUSNS6uEm33dKzp4kay0p4em7kLmsMu4zp/Z9ICDlo1CdK+
+IzKZQMjVbwfowe6i+I0wVYTTRhSGignjErI5sw7jp1PLUIwYswj+tm0QFi5b40Bj
++xgnJaqX6SCQ9tE8mdewFIqyrzIikQAcWp1tl3T8jHogAnubuRmMjt+BFvgfif8o
+AtPC4OBOWnp2K4Ci8ZOqvH0iFYxiHofftRw3nrIdQYcOD7dtv6CmM6FhxZtg9DMl
+R8x9igyDtzaPp7FKgHaMxLtdxo5De3vgIoQWdKG1tWLxMt5DCOPHpis0MobeiYMv
+D9taEWhR7lgkn9ONep8eUNvWKcDZQ6m8Lyl0JyWn2z2kj3Pt+pEcunof7xaDz5sS
+daJ1j+TzGXUXZuHI/dSxHi1ZluexNi8x5B81kTbd7/VxxNc6C/Rgara+qwoo9hyP
+HEdQFQ==
+=/gwj
+-----END PGP PUBLIC KEY BLOCK-----
+
+OBS security:shibboleth RPM repository key
+
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: GnuPG v1.4.5 (GNU/Linux)
+
+mQGiBEholYgRBACW/kTYZi5mAEMP8j1qui2bRnWmYblFbiZvb5JMJYUWL/jyMjHj
+LLJvVrnOre/AxexH9KmaJwBuNoa/X9l/tQGIb49QRhR346QQUbQcwlYpckg5ccqN
+qlAURHEdjCxRxMhzPs+C/F6Nqa/fHpAectW0JNRqAVVd9CWjCG3l6I2CywCgk9UI
+SUAvaB5bVMxEVrFAKrVh4MUEAISeUwOaTIZftIamjo0VrnYemHS4SmGqMALEtHeG
+/o7ecMhLb/MvreEVISrE1hbfmnObYoiVXWJrorOEZDh1hOVdGRkHJOYHvQSRp9uC
+/uy4Mmog4R7ba5Ct5wpw0RCav2HMqwOJEyCX6jnip5P5LMmaFdGg/RQZM/e9mbyF
+VeL0A/0WK//+VWrQya1rQIG+v4IZ+rWvrkqXKtrzELkWd/3vWNMXi4BfeBM/itLQ
+yYOdhnx55FHxBzv+dK+UErrdTM8Ingljy7oztV/G6+K15CcGvEy9ITb62v2bDdS2
+uYWEFOQJK+I6aU4paNysvtKsOlTt9FhyfJJW9G3kJDUBPM9HbLRIc2VjdXJpdHk6
+c2hpYmJvbGV0aCBPQlMgUHJvamVjdCA8c2VjdXJpdHk6c2hpYmJvbGV0aEBidWls
+ZC5vcGVuc3VzZS5vcmc+iGYEExECACYFAlgaKTsCGwMFCRPQQ7MGCwkIBwMCBBUC
+CAMEFgIDAQIeAQIXgAAKCRBzyTdFfQobPWC4AJ96FdlQrvlAHzQjqT994h6lZxfZ
+pgCfSBPRhrdNGh72+d288vgZ7ptV4/SIRgQTEQIABgUCSGiViAAKCRA7MBG3a51l
+Iy3vAKCUPurlZup+vzQtpij3FMo0JAVW9ACgjdkt/hWt0WsjfHb+/cPwXtgx9X8=
+=6fV2
+-----END PGP PUBLIC KEY BLOCK-----
+
+Henri Mikkonen PGP key
+
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+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
+A+i1fd0Gifis1HhBeA==
+=ObHY
+-----END PGP PUBLIC KEY BLOCK-----
+
+philsmart GPG key 4096-bit RSA key, ID C21771DD, created 2020-02-25
+
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: GnuPG v2.0.22 (GNU/Linux)
+
+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
+3aNjQSVwGC4Owz97QpUq9StZb1qhvIw=
+=1e4x
+-----END PGP PUBLIC KEY BLOCK-----

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list