[java-identity-provider] branch main updated: IDP-1664 - Support Module service API

Scott Cantor cantor.2 at osu.edu
Tue Sep 1 21:30:58 UTC 2020


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

scantor pushed a commit to branch main
in repository java-identity-provider.

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

The following commit(s) were added to refs/heads/main by this push:
       new  20d5dedd5 IDP-1664 - Support Module service API
20d5dedd5 is described below

commit 20d5dedd536583705b53938881dcfeb1e284c8be
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Tue Sep 1 17:29:11 2020 -0400

    IDP-1664 - Support Module service API
    
    https://issues.shibboleth.net/jira/browse/IDP-1664
    
    First set of draft code.
---
 .../shibboleth/idp/module/AbstractIdPModule.java   | 330 +++++++++++++++++++++
 .../java/net/shibboleth/idp/module/IdPModule.java  | 169 +++++++++++
 .../net/shibboleth/idp/module/ModuleException.java |  63 ++++
 .../idp/module/PropertyDrivenIdPModule.java        | 199 +++++++++++++
 .../net/shibboleth/idp/module/package-info.java    |  22 ++
 idp-conf-impl/pom.xml                              |   5 +
 .../net/shibboleth/idp/module/impl/BadModule.java  |  40 +++
 .../net/shibboleth/idp/module/impl/BadModule2.java |  40 +++
 .../shibboleth/idp/module/impl/IdPModuleTest.java  |  90 ++++++
 .../net/shibboleth/idp/module/impl/TestModule.java |  40 +++
 .../services/net.shibboleth.idp.module.IdPModule   |   3 +
 .../shibboleth/idp/module/impl/module.properties   |  25 ++
 12 files changed, 1026 insertions(+)

diff --git a/idp-admin-api/src/main/java/net/shibboleth/idp/module/AbstractIdPModule.java b/idp-admin-api/src/main/java/net/shibboleth/idp/module/AbstractIdPModule.java
new file mode 100644
index 000000000..9a68ab59e
--- /dev/null
+++ b/idp-admin-api/src/main/java/net/shibboleth/idp/module/AbstractIdPModule.java
@@ -0,0 +1,330 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.module;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Path;
+import java.security.DigestOutputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotLive;
+import net.shibboleth.utilities.java.support.annotation.constraint.Unmodifiable;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
+
+/**
+ * {@link IdPModule} base class implementing basic file management.
+ */
+public abstract class AbstractIdPModule implements IdPModule {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(AbstractIdPModule.class);
+    
+    /** Module resources. */
+    @Nonnull @NonnullElements private Collection<BasicModuleResource> moduleResources;
+    
+    /** Constructor. */
+    public AbstractIdPModule() {
+        moduleResources = Collections.emptyList();
+    }
+    
+    /** {@inheritDoc} */
+    @Nonnull @NonnullElements @NotLive @Unmodifiable public Collection<ModuleResource> getResources() {
+        return List.copyOf(moduleResources);
+    }
+    
+    /**
+     * Sets the module resources to manage.
+     * 
+     * @param resources resources to manage
+     */
+    public void setResources(@Nullable @NonnullElements final Collection<BasicModuleResource> resources) {
+        if (resources != null) {
+            moduleResources = List.copyOf(resources);
+        } else {
+            moduleResources = Collections.emptyList();
+        }
+    }
+    
+    /** {@inheritDoc} */
+    public boolean isEnabled(@Nonnull final ModuleContext moduleContext) {
+        
+        log.debug("Module {} checking enabled status", getId());
+        
+        if (moduleResources.isEmpty()) {
+            log.debug("Module {} is always enabled", getId());
+            return true;
+        }
+
+        for (final ModuleResource resource : moduleResources) {
+            final Path resolved = moduleContext.getIdPHome().resolve(resource.getDestination());
+            log.debug("Module {}: resolved resource destination {}", getId(), resolved);
+            if (resolved.toFile().exists()) {
+                log.debug("Module {}: resource destination {} exists, module is enabled", getId(), resolved);
+                return true;
+            }
+        }
+        
+        log.debug("Module {} is not enabled", getId());
+        return false;
+    }
+    
+    /** {@inheritDoc} */
+    public void enable(@Nonnull final ModuleContext moduleContext) throws ModuleException {
+        if (isHttpClientRequired() && moduleContext.getHttpClient() == null) {
+            throw new ModuleException("HTTP client required but not available");
+        }
+        
+        log.debug("Module {} enabling", getId());
+        for (final ModuleResource resource : moduleResources) {
+            ((BasicModuleResource) resource).enable(moduleContext);
+        }
+        log.info("Module {} enabled", getId());
+    }
+
+    /** {@inheritDoc} */
+    public void disable(@Nonnull final ModuleContext moduleContext, final boolean clean) throws ModuleException {
+        log.debug("Module {} disabling", getId());
+        for (final ModuleResource resource : moduleResources) {
+            ((BasicModuleResource) resource).disable(moduleContext, clean);
+        }
+        log.info("Module {} disabled", getId());
+    }
+    
+    /**
+     * Models a specific resource managed by a module.
+     */
+    class BasicModuleResource implements ModuleResource {
+        
+        /** Source. */
+        @Nonnull @NotEmpty private final String source;
+        
+        /** Destination. */
+        @Nonnull private final Path destination;
+        
+        /** Replacement criteria. */
+        private final boolean replace;
+        
+        /**
+         * Constructor.
+         *
+         * @param src source
+         * @param dest destination
+         * @param shouldReplace whether to replace when enabling
+         */
+        public BasicModuleResource(@Nonnull @NotEmpty final String src, @Nonnull final Path dest,
+                final boolean shouldReplace) {
+            source = Constraint.isNotNull(StringSupport.trimOrNull(src), "Source cannot be null");
+            destination = Constraint.isNotNull(dest, "Destination cannot be null");
+            replace = shouldReplace;
+        }
+
+        /** {@inheritDoc} */
+        @Nonnull public String getSource() {
+            return source;
+        }
+        
+        /** {@inheritDoc} */
+        @Nonnull public Path getDestination() {
+            return destination;
+        }
+        
+        /** {@inheritDoc} */
+        public boolean isReplace() {
+            return replace;
+        }
+        
+        /**
+         * Gets whether the resource has been altered at its destination from the source material.
+         * 
+         * @param moduleContext context for module operations
+         * 
+         * @return true iff the resource has been changed
+         */
+        public boolean hasChanged(@Nonnull final ModuleContext moduleContext) {
+            try (final InputStream dest = getDestinationStream(moduleContext)) {
+                
+                if (dest != null) {
+                    final byte[] destHash;
+                    
+                    final MessageDigest digest = MessageDigest.getInstance("SHA1");
+                    try (final OutputStream destSink = OutputStream.nullOutputStream();
+                            final DigestOutputStream destDigest = new DigestOutputStream(destSink, digest)) {
+                        dest.transferTo(destDigest);
+                        destHash = digest.digest();
+                    }
+                    
+                    try (final InputStream src = getSourceStream(moduleContext)) {
+                        if (src != null) {
+                            try (final OutputStream srcSink = OutputStream.nullOutputStream();
+                                    final DigestOutputStream srcDigest = new DigestOutputStream(srcSink, digest)) {
+                                src.transferTo(srcDigest);
+                                return digest.digest().equals(destHash);
+                            }
+                        }
+                        log.debug("Module {} resource {} does not exist at source", getId(), source);
+                        return true;
+                    }
+                }
+                
+                log.debug("Module {} resource {} does not exist at destination", getId(), source);
+                return false;
+            } catch (final IOException e) {
+                log.error("Module {} resource {} raised error while checking contents", getId(), source, e);
+                return true;
+            } catch (final NoSuchAlgorithmException e) {
+                log.error("Module {} resource {} raised error while checking contents", getId(), source, e);
+                return true;
+            }
+        }
+        
+        /**
+         * Access the source as a stream.
+         * 
+         * @param moduleContext context for module operations
+         * 
+         * @return a stream or null if the source does not exist
+         * 
+         * @throws IOException on failure
+         */
+        @Nullable private InputStream getSourceStream(@Nonnull final ModuleContext moduleContext)
+                throws IOException {
+            
+            if (source.startsWith("https://") || source.startsWith("http://")) {
+                // TODO http
+                return null;
+            }
+            return getClass().getResourceAsStream(source);
+        }
+
+        /**
+         * Access the destination as a stream.
+         * 
+         * @param moduleContext context for module operations
+         * 
+         * @return a stream or null if the destination does not exist
+         * 
+         * @throws IOException on failure
+         */
+        @Nullable private InputStream getDestinationStream(@Nonnull final ModuleContext moduleContext)
+                throws IOException {
+            final File destFile = moduleContext.getIdPHome().resolve(destination).toFile();
+            if (destFile.exists()) {
+                return new FileInputStream(destFile);
+            }
+            return null;
+        }
+
+        /**
+         * Enable the supplied resource.
+         * 
+         * @param moduleContext module context
+         * 
+         * @throws ModuleException if an error occurs
+         */
+        private void enable(@Nonnull final ModuleContext moduleContext) throws ModuleException {
+            log.debug("Module {} enabling resource {}", getId(), source);
+
+            final boolean hasChanged = hasChanged(moduleContext);
+            
+            try (final InputStream srcStream = getSourceStream(moduleContext)) {
+                if (srcStream == null) {
+                    throw new IOException("Source stream was null");
+                }
+
+                
+                final File destFile;
+                
+                if (hasChanged) {
+                    if (isReplace()) {
+                        final File renamedFile = moduleContext.getIdPHome().resolve(destination).toFile();
+                        if (renamedFile.renameTo(new File(renamedFile.getPath() + ".idpsave"))) {
+                            log.info("Module {} preserved {}", getId(), renamedFile);
+                        } else {
+                            throw new ModuleException("Unable to rename " + renamedFile);
+                        }
+                        destFile = moduleContext.getIdPHome().resolve(destination).toFile();
+                    } else {
+                        destFile = new File(moduleContext.getIdPHome().resolve(destination).toString() + ".idpnew");
+                    }
+                    
+                } else {
+                    destFile = moduleContext.getIdPHome().resolve(destination).toFile();
+                }
+
+                try (final OutputStream destStream = new FileOutputStream(destFile)) {
+                    srcStream.transferTo(destStream);
+                    log.info("Module {} created {}", getId(), destFile);
+                }
+
+            } catch (final IOException e) {
+                log.error("Module {} unable to enable resource {}", getId(), source, e);
+                throw new ModuleException(e);
+            }
+        }
+
+        /**
+         * Disable the supplied resource, either removing or renaming.
+         * 
+         * @param moduleContext module context
+         * @param clean true iff resource should be removed
+         * 
+         * @throws ModuleException if an error occurs
+         */
+        private void disable(@Nonnull final ModuleContext moduleContext, final boolean clean) throws ModuleException {
+            
+            final Path resolved = moduleContext.getIdPHome().resolve(destination);
+            log.debug("Module {} resolved resource destination {}", getId(), resolved);
+            final File file = resolved.toFile();
+            if (file.exists()) {
+                if (clean) {
+                    log.info("Module {} removing resource {}", getId(), file);
+                    if (!file.delete()) {
+                        throw new ModuleException("Unable to remove resource " + file);
+                    }
+                } else {
+                    log.info("Module {} moving aside resource {}", getId(), file);
+                    if (!file.renameTo(new File(file.toString() + ".idpsave"))) {
+                        throw new ModuleException("Unable to rename resource " + file);
+                    }
+                }
+            } else {
+                log.info("Module {} resource {} missing, ignoring", getId(), file);
+            }
+        }
+        
+    }
+
+}
\ No newline at end of file
diff --git a/idp-admin-api/src/main/java/net/shibboleth/idp/module/IdPModule.java b/idp-admin-api/src/main/java/net/shibboleth/idp/module/IdPModule.java
new file mode 100644
index 000000000..b89dbc3c7
--- /dev/null
+++ b/idp-admin-api/src/main/java/net/shibboleth/idp/module/IdPModule.java
@@ -0,0 +1,169 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.module;
+
+import java.net.URL;
+import java.nio.file.Path;
+import java.util.Collection;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.apache.http.client.HttpClient;
+import org.opensaml.security.httpclient.HttpClientSecurityParameters;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotLive;
+import net.shibboleth.utilities.java.support.annotation.constraint.Unmodifiable;
+import net.shibboleth.utilities.java.support.component.IdentifiedComponent;
+
+/**
+ * This interface is exported (via the service API) by every IdP module.
+ */
+public interface IdPModule extends IdentifiedComponent {
+    
+    /**
+     * Gets module name.
+     * 
+     * @return a human-readable name for the module
+     */
+    @Nonnull @NotEmpty String getName();
+    
+    /**
+     * Gets module description.
+     * 
+     * @return a human-readable description for the module
+     */
+    @Nullable @NotEmpty String getDescription();
+
+    /**
+     * Gets module URL.
+     * 
+     * @return a URL for obtaining additional information about the module
+     */
+    @Nullable URL getURL();
+    
+    /**
+     * Gets whether module enablement requires access to an {@link HttpClient}.
+     * 
+     * @return true iff enabling the module requires HTTP client
+     */
+    boolean isHttpClientRequired();
+
+    /**
+     * Gets resources managed by this module.
+     * 
+     * @return resources managed by this module
+     */
+    @Nonnull @NonnullElements @NotLive @Unmodifiable public Collection<ModuleResource> getResources();
+    
+    /**
+     * Gets whether the module is enabled.
+     * 
+     * <p>The status of "enabled" is meant to reflect whether a deployer has previously
+     * or implicitly enabled the module, not necessarily whether the module is fully or
+     * properly configured or in use.</p>
+     * 
+     * @param moduleContext module context
+     * 
+     * @return true iff the module is enabled
+     */
+    boolean isEnabled(@Nonnull final ModuleContext moduleContext);
+    
+    /**
+     * Enable the module.
+     * 
+     * <p>This operation MUST be idempotent.</p>
+     * 
+     * @param moduleContext module context
+     * 
+     * @throws ModuleException if not successful 
+     */
+    void enable(@Nonnull final ModuleContext moduleContext) throws ModuleException;
+
+    /**
+     * Disable the module.
+     * 
+     * <p>This operation MUST be idempotent with respect to the value of the input parameter.</p>
+     * 
+     * @param moduleContext module context
+     * @param clean if true, the module should attempt to fully remove traces of previous
+     *  use in a potentially destructive fashion
+     * 
+     * @throws ModuleException if not successful 
+     */
+    void disable(@Nonnull final ModuleContext moduleContext, final boolean clean) throws ModuleException;
+
+    /**
+     * Interface to information required to perform some module operations.
+     */
+    interface ModuleContext {
+
+        /**
+         * Gets software installation location.
+         * 
+         * @return install path
+         */
+        @Nonnull @NotEmpty Path getIdPHome();
+        
+        /**
+         * Gets an {@link HttpClient} instance to use if available.
+         * 
+         * @return HTTP client instance
+         */
+        @Nullable HttpClient getHttpClient();
+
+        /**
+         * Gets {@link HttpClient} security parameters, if any.
+         * 
+         * @return HTTP client security parameters to use
+         */
+        @Nullable HttpClientSecurityParameters getHttpClientSecurityParameters();
+    }
+    
+    /**
+     * Interface to a resource managed by the module.
+     */
+    interface ModuleResource {
+        
+        /**
+         * Gets the source location of the resource.
+         * 
+         * <p>This may be a URL or a local path that will be assumed a classpath.</p>
+         * 
+         * @return source location
+         */
+        @Nonnull public String getSource();
+        
+        /**
+         * Gets the destination for the resource.
+         * 
+         * @return destination path
+         */
+        @Nonnull public Path getDestination();
+        
+        /**
+         * Gets whether the resource should be config(replace) or config(noreplace) in RPM specfile parlance.
+         * 
+         * @return true iff the resource should be replaced with the original preserved
+         */
+        public boolean isReplace();
+    }
+    
+}
\ No newline at end of file
diff --git a/idp-admin-api/src/main/java/net/shibboleth/idp/module/ModuleException.java b/idp-admin-api/src/main/java/net/shibboleth/idp/module/ModuleException.java
new file mode 100644
index 000000000..bf69147fb
--- /dev/null
+++ b/idp-admin-api/src/main/java/net/shibboleth/idp/module/ModuleException.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.module;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+/** Module exception class. */
+ at ThreadSafe
+public class ModuleException extends Exception {
+
+    /** Serial number. */
+    private static final long serialVersionUID = 2811724801157828005L;
+
+    /** Constructor. */
+    public ModuleException() {
+        
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param message exception message
+     */
+    public ModuleException(@Nullable final String message) {
+        super(message);
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param wrappedException exception to be wrapped by this one
+     */
+    public ModuleException(@Nullable final Exception wrappedException) {
+        super(wrappedException);
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param message exception message
+     * @param wrappedException exception to be wrapped by this one
+     */
+    public ModuleException(@Nullable final String message, @Nullable final Exception wrappedException) {
+        super(message, wrappedException);
+    }
+    
+}
\ No newline at end of file
diff --git a/idp-admin-api/src/main/java/net/shibboleth/idp/module/PropertyDrivenIdPModule.java b/idp-admin-api/src/main/java/net/shibboleth/idp/module/PropertyDrivenIdPModule.java
new file mode 100644
index 000000000..36928391e
--- /dev/null
+++ b/idp-admin-api/src/main/java/net/shibboleth/idp/module/PropertyDrivenIdPModule.java
@@ -0,0 +1,199 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.module;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Properties;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.logic.ConstraintViolationException;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
+
+/**
+ * Implementation of {@link IdPModule} relying on Java {@link Properties}.
+ */
+public class PropertyDrivenIdPModule extends AbstractIdPModule {
+
+    /** Default name of module properties resource. */
+    @Nonnull @NotEmpty public static final String DEFAULT_RESOURCE = "module.properties";
+
+    /** Suffix of property for module name. */
+    @Nonnull @NotEmpty public static final String MODULE_NAME_PROPERTY = ".name";
+
+    /** Suffix of property for module description. */
+    @Nonnull @NotEmpty public static final String MODULE_DESC_PROPERTY = ".desc";
+
+    /** Suffix of property for module URL. */
+    @Nonnull @NotEmpty public static final String MODULE_URL_PROPERTY = ".url";
+
+    /** Suffix of property for resource source. */
+    @Nonnull @NotEmpty public static final String MODULE_SRC_PROPERTY = ".src";
+
+    /** Suffix of property for resource destination. */
+    @Nonnull @NotEmpty public static final String MODULE_DEST_PROPERTY = ".dest";
+
+    /** Suffix of property for resource replacement. */
+    @Nonnull @NotEmpty public static final String MODULE_REPLACE_PROPERTY = ".replace";
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(PropertyDrivenIdPModule.class);
+
+    /** Properties describing module. */
+    @Nonnull private final Properties moduleProperties;
+    
+    /** Module name. */
+    @Nonnull @NotEmpty private String moduleName;
+
+    /** Module description. */
+    @Nullable @NotEmpty private String moduleDesc;
+
+    /** Module URL. */
+    @Nullable private URL moduleURL;
+    
+    /** Whether to require an HTTP client. */
+    private boolean requireHttpClient;
+
+    /**
+     * Constructor.
+     *
+     * @param claz type of object used to locate default module.properties resource
+     * 
+     * @throws IOException if unable to read file
+     * @throws ModuleException if the module is not in a valid state
+     */
+    public PropertyDrivenIdPModule(@Nonnull final Class<? extends IdPModule> claz) throws IOException, ModuleException {
+        this(claz.getResourceAsStream(DEFAULT_RESOURCE));
+    }
+    
+    /**
+     * Constructor.
+     *
+     * @param inputStream property stream
+     * 
+     * @throws IOException if unable to read file
+     * @throws ModuleException if the module is not in a valid state
+     */
+    public PropertyDrivenIdPModule(@Nonnull final InputStream inputStream)
+            throws IOException, ModuleException {
+        moduleProperties = new Properties();
+        moduleProperties.load(inputStream);
+        load();
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param properties property set
+     * 
+     * @throws ModuleException if the module is not in a valid state
+     */
+    public PropertyDrivenIdPModule(@Nonnull final Properties properties) throws ModuleException {
+        moduleProperties = Constraint.isNotNull(properties, "Properties cannot be null");
+        load();
+    }
+
+// Checkstyle: CyclomaticComplexity OFF
+    protected void load() throws ModuleException {
+        try {
+            moduleName = Constraint.isNotNull(
+                    StringSupport.trimOrNull(moduleProperties.getProperty(getId() + MODULE_NAME_PROPERTY)),
+                    "Module name missing from properties");
+            moduleDesc = StringSupport.trimOrNull(moduleProperties.getProperty(getId() + MODULE_DESC_PROPERTY));
+            final String url = StringSupport.trimOrNull(moduleProperties.getProperty(getId() + MODULE_URL_PROPERTY));
+            if (url != null) {
+                moduleURL = new URL(url);
+            }
+            
+            final Collection<BasicModuleResource> resources = new ArrayList<>();
+            
+            for (Integer rnum = 1; ; ++rnum) {
+                
+                final String renumstr = "." + rnum.toString();
+                
+                final String src = moduleProperties.getProperty(getId() + renumstr + MODULE_SRC_PROPERTY);
+                final String dest = moduleProperties.getProperty(getId() + renumstr + MODULE_DEST_PROPERTY);
+                if (src == null || dest == null) {
+                    break;
+                }
+                
+                final Boolean replace = Boolean.valueOf(
+                        moduleProperties.getProperty(getId() + renumstr + MODULE_REPLACE_PROPERTY, "false"));
+                
+                final Path destPath = Path.of(dest);
+                if (dest.contains("..") || destPath.isAbsolute()) {
+                    throw new ModuleException("Module contained a suspect resource destination");
+                }
+                
+                if (!requireHttpClient) {
+                    requireHttpClient = src.startsWith("https://") || src.startsWith("http://");
+                }
+                
+                resources.add(new BasicModuleResource(src, destPath, replace));
+            }
+            
+            setResources(resources);
+            
+            log.debug("Module {} loaded", getId());
+            resources.forEach(
+                    r -> log.debug("Module {}: Resource {} -> {} ({})",
+                            getId(), r.getSource(), r.getDestination(), r.isReplace() ? "replace" : "noreplace"));
+        } catch (final ConstraintViolationException | MalformedURLException e) {
+            throw new ModuleException(e);
+        }
+    }
+// Checkstyle: CyclomaticComplexity ON
+    
+    /** {@inheritDoc} */
+    public String getId() {
+        return getClass().getName();
+    }
+
+    /** {@inheritDoc} */
+    @Nonnull @NotEmpty public String getName() {
+        return moduleName;
+    }
+    
+    /** {@inheritDoc} */
+    @Nullable @NotEmpty public String getDescription() {
+        return moduleDesc;
+    }
+
+    /** {@inheritDoc} */
+    @Nullable public URL getURL() {
+        return moduleURL;
+    }
+    
+    /** {@inheritDoc} */
+    public boolean isHttpClientRequired() {
+        return requireHttpClient;
+    }
+
+}
\ No newline at end of file
diff --git a/idp-admin-api/src/main/java/net/shibboleth/idp/module/package-info.java b/idp-admin-api/src/main/java/net/shibboleth/idp/module/package-info.java
new file mode 100644
index 000000000..7314ce438
--- /dev/null
+++ b/idp-admin-api/src/main/java/net/shibboleth/idp/module/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * APIs for representing units of IdP functionality as modules.
+ */
+
+package net.shibboleth.idp.module;
\ No newline at end of file
diff --git a/idp-conf-impl/pom.xml b/idp-conf-impl/pom.xml
index 2beadaddc..d765d89bc 100644
--- a/idp-conf-impl/pom.xml
+++ b/idp-conf-impl/pom.xml
@@ -22,6 +22,11 @@
 
     <dependencies>
         <!-- Compile Dependencies -->
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>idp-admin-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
 
         <!-- Provided Dependencies -->
 
diff --git a/idp-conf-impl/src/test/java/net/shibboleth/idp/module/impl/BadModule.java b/idp-conf-impl/src/test/java/net/shibboleth/idp/module/impl/BadModule.java
new file mode 100644
index 000000000..8c7857051
--- /dev/null
+++ b/idp-conf-impl/src/test/java/net/shibboleth/idp/module/impl/BadModule.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.module.impl;
+
+import java.io.IOException;
+
+import net.shibboleth.idp.module.ModuleException;
+import net.shibboleth.idp.module.PropertyDrivenIdPModule;
+
+/**
+ * Test {@IdPModule} implementation.
+ */
+public final class BadModule extends PropertyDrivenIdPModule {
+
+    /**
+     * Constructor.
+     *  
+     * @throws ModuleException 
+     * @throws IOException
+     */
+    public BadModule() throws IOException, ModuleException {
+        super(BadModule.class);
+    }
+
+}
\ No newline at end of file
diff --git a/idp-conf-impl/src/test/java/net/shibboleth/idp/module/impl/BadModule2.java b/idp-conf-impl/src/test/java/net/shibboleth/idp/module/impl/BadModule2.java
new file mode 100644
index 000000000..a9f45aad5
--- /dev/null
+++ b/idp-conf-impl/src/test/java/net/shibboleth/idp/module/impl/BadModule2.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.module.impl;
+
+import java.io.IOException;
+
+import net.shibboleth.idp.module.ModuleException;
+import net.shibboleth.idp.module.PropertyDrivenIdPModule;
+
+/**
+ * Test {@IdPModule} implementation.
+ */
+public final class BadModule2 extends PropertyDrivenIdPModule {
+
+    /**
+     * Constructor.
+     *  
+     * @throws ModuleException 
+     * @throws IOException
+     */
+    public BadModule2() throws IOException, ModuleException {
+        super(BadModule2.class);
+    }
+
+}
\ No newline at end of file
diff --git a/idp-conf-impl/src/test/java/net/shibboleth/idp/module/impl/IdPModuleTest.java b/idp-conf-impl/src/test/java/net/shibboleth/idp/module/impl/IdPModuleTest.java
new file mode 100644
index 000000000..633a51eb8
--- /dev/null
+++ b/idp-conf-impl/src/test/java/net/shibboleth/idp/module/impl/IdPModuleTest.java
@@ -0,0 +1,90 @@
+package net.shibboleth.idp.module.impl;
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.nio.file.Path;
+import java.util.Iterator;
+import java.util.Optional;
+import java.util.ServiceConfigurationError;
+import java.util.ServiceLoader;
+import java.util.ServiceLoader.Provider;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import net.shibboleth.idp.module.IdPModule;
+import net.shibboleth.idp.module.IdPModule.ModuleResource;
+import net.shibboleth.idp.module.ModuleException;
+
+/**
+ * Unit tests exercising module code.
+ */
+public class IdPModuleTest {
+
+    @Test
+    public void testModule() {
+        
+        final ServiceLoader<IdPModule> loader = ServiceLoader.load(IdPModule.class);
+        final Optional<Provider<IdPModule>> opt =
+                loader.stream().filter(p -> TestModule.class.equals(p.type())).findFirst();
+        
+        Assert.assertTrue(opt.isPresent());
+        
+        final IdPModule module = opt.get().get();
+        
+        Assert.assertEquals(module.getId(), TestModule.class.getName());
+        Assert.assertEquals(module.getName(), "Test module");
+        Assert.assertEquals(module.getURL().toString(), "https://wiki.shibboleth.net/confluence/display/IDP4/Home");
+        
+        final Iterator<ModuleResource> resources = module.getResources().iterator();
+        Assert.assertEquals(module.getResources().size(), 2);
+        
+        ModuleResource resource = resources.next();
+        Assert.assertEquals(resource.getSource(), "net/shibboleth/idp/module/impl/test.xml");
+        Assert.assertEquals(resource.getDestination(), Path.of("conf/test.xml"));
+        
+        resource = resources.next();
+        Assert.assertEquals(resource.getSource(), "net/shibboleth/idp/module/impl/test.vm");
+        Assert.assertEquals(resource.getDestination(), Path.of("views/test.vm"));
+    }
+
+    @Test
+    public void testBadModules() {
+        
+        final ServiceLoader<IdPModule> loader = ServiceLoader.load(IdPModule.class);
+
+        Optional<Provider<IdPModule>> opt =
+                loader.stream().filter(p -> BadModule.class.equals(p.type())).findFirst();
+        Assert.assertTrue(opt.isPresent());
+        try {
+            opt.get().get();
+            Assert.fail("BadModule should have failed");
+        } catch (final ServiceConfigurationError e) {
+            Assert.assertTrue(e.getCause() instanceof ModuleException);
+        }
+
+        opt = loader.stream().filter(p -> BadModule2.class.equals(p.type())).findFirst();
+        Assert.assertTrue(opt.isPresent());
+        try {
+            opt.get().get();
+            Assert.fail("BadModule2 should have failed");
+        } catch (final ServiceConfigurationError e) {
+            Assert.assertTrue(e.getCause() instanceof ModuleException);
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/idp-conf-impl/src/test/java/net/shibboleth/idp/module/impl/TestModule.java b/idp-conf-impl/src/test/java/net/shibboleth/idp/module/impl/TestModule.java
new file mode 100644
index 000000000..e1e65d77a
--- /dev/null
+++ b/idp-conf-impl/src/test/java/net/shibboleth/idp/module/impl/TestModule.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.module.impl;
+
+import java.io.IOException;
+
+import net.shibboleth.idp.module.ModuleException;
+import net.shibboleth.idp.module.PropertyDrivenIdPModule;
+
+/**
+ * Test {@IdPModule} implementation.
+ */
+public final class TestModule extends PropertyDrivenIdPModule {
+
+    /**
+     * Constructor.
+     *  
+     * @throws ModuleException 
+     * @throws IOException
+     */
+    public TestModule() throws IOException, ModuleException {
+        super(TestModule.class);
+    }
+
+}
\ No newline at end of file
diff --git a/idp-conf-impl/src/test/resources/META-INF/services/net.shibboleth.idp.module.IdPModule b/idp-conf-impl/src/test/resources/META-INF/services/net.shibboleth.idp.module.IdPModule
new file mode 100644
index 000000000..5a9d19fb3
--- /dev/null
+++ b/idp-conf-impl/src/test/resources/META-INF/services/net.shibboleth.idp.module.IdPModule
@@ -0,0 +1,3 @@
+net.shibboleth.idp.module.impl.TestModule
+net.shibboleth.idp.module.impl.BadModule
+net.shibboleth.idp.module.impl.BadModule2
diff --git a/idp-conf-impl/src/test/resources/net/shibboleth/idp/module/impl/module.properties b/idp-conf-impl/src/test/resources/net/shibboleth/idp/module/impl/module.properties
new file mode 100644
index 000000000..7ef368c67
--- /dev/null
+++ b/idp-conf-impl/src/test/resources/net/shibboleth/idp/module/impl/module.properties
@@ -0,0 +1,25 @@
+# Unit test modules
+
+# Main test module
+
+net.shibboleth.idp.module.impl.TestModule.name = Test module
+net.shibboleth.idp.module.impl.TestModule.desc = Module for unit tests
+net.shibboleth.idp.module.impl.TestModule.url = https://wiki.shibboleth.net/confluence/display/IDP4/Home
+
+net.shibboleth.idp.module.impl.TestModule.1.src = net/shibboleth/idp/module/impl/test.xml
+net.shibboleth.idp.module.impl.TestModule.1.dest = conf/test.xml
+
+net.shibboleth.idp.module.impl.TestModule.2.src = net/shibboleth/idp/module/impl/test.vm
+net.shibboleth.idp.module.impl.TestModule.2.dest = views/test.vm
+
+# Broken modules due to dangerous resources
+
+net.shibboleth.idp.module.impl.BadModule.name = Bad module
+net.shibboleth.idp.module.impl.BadModule.desc = Module for unit tests with error
+net.shibboleth.idp.module.impl.BadModule.1.src = net/shibboleth/idp/module/impl/test.xml
+net.shibboleth.idp.module.impl.BadModule.1.dest = ../conf/test.xml
+
+net.shibboleth.idp.module.impl.BadModule2.name = Bad module 2
+net.shibboleth.idp.module.impl.BadModule2.desc = Module for unit tests with error
+net.shibboleth.idp.module.impl.BadModule2.1.src = net/shibboleth/idp/module/impl/test.xml
+net.shibboleth.idp.module.impl.BadModule2.1.dest = /conf/test.xml

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


More information about the commits mailing list