[java-shib-profile] branch main updated: IDP-2121 Future Proofing the Module Plugin infrastructure for Future SP use

Rod Widdowson rdw at steadingsoftware.com
Tue Jun 6 13:58:37 UTC 2023


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

rdw pushed a commit to branch main
in repository java-shib-profile.

View the commit online:
http://git.shibboleth.net/view/?p=java-shib-profile.git;a=commit;h=79bbe6a14577e7e7efc3c950a604e8a4095533d4

The following commit(s) were added to refs/heads/main by this push:
     new 79bbe6a  IDP-2121 Future Proofing the Module Plugin infrastructure for Future SP use
79bbe6a is described below

commit 79bbe6a14577e7e7efc3c950a604e8a4095533d4
Author: Rod Widdowson <rdw at steadingsoftware.com>
AuthorDate: Tue Jun 6 13:53:23 2023 +0100

    IDP-2121 Future Proofing the Module Plugin infrastructure for Future SP use
    
    https://shibboleth.atlassian.net/browse/IDP-2121
    
    Move the Module stuff down from the IdP to java-shib-profile
---
 shib-profile-api/pom.xml                           |  19 +
 .../shibboleth/profile/module/AbstractModule.java  | 610 +++++++++++++++++++++
 .../java/net/shibboleth/profile/module/Module.java | 208 +++++++
 .../shibboleth/profile/module/ModuleContext.java   | 157 ++++++
 .../shibboleth/profile/module/ModuleException.java |  66 +++
 .../shibboleth/profile/module/package-info.java    |  24 +
 6 files changed, 1084 insertions(+)

diff --git a/shib-profile-api/pom.xml b/shib-profile-api/pom.xml
index b3c310b..8e6dcb3 100644
--- a/shib-profile-api/pom.xml
+++ b/shib-profile-api/pom.xml
@@ -56,6 +56,10 @@
             <groupId>${shib-shared.groupId}</groupId>
             <artifactId>shib-service</artifactId>
         </dependency>
+        <dependency>
+            <groupId>${shib-shared.groupId}</groupId>
+            <artifactId>shib-networking-spring</artifactId>
+        </dependency>
         <dependency>
             <groupId>${shib-shared.groupId}</groupId>
             <artifactId>shib-spring</artifactId>
@@ -70,11 +74,26 @@
             <artifactId>spring-expression</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+        </dependency>
+
         <dependency>
             <groupId>io.dropwizard.metrics</groupId>
             <artifactId>metrics-core</artifactId>
         </dependency>
 
+	<dependency>
+            <groupId>${httpclient.groupId}</groupId>
+            <artifactId>${httpclient.artifactId}</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>${httpclient.httpcore.groupId}</groupId>
+            <artifactId>${httpclient.httpcore.artifactId}</artifactId>
+        </dependency>
+
         <dependency>
             <groupId>com.google.guava</groupId>
             <artifactId>guava</artifactId>
diff --git a/shib-profile-api/src/main/java/net/shibboleth/profile/module/AbstractModule.java b/shib-profile-api/src/main/java/net/shibboleth/profile/module/AbstractModule.java
new file mode 100644
index 0000000..6d9c7fc
--- /dev/null
+++ b/shib-profile-api/src/main/java/net/shibboleth/profile/module/AbstractModule.java
@@ -0,0 +1,610 @@
+/*
+ * 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.profile.module;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.FileAlreadyExistsException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.nio.file.StandardOpenOption;
+import java.security.DigestOutputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.apache.commons.lang3.SystemUtils;
+
+import org.apache.hc.client5.http.classic.methods.HttpGet;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.opensaml.security.httpclient.HttpClientSecuritySupport;
+import org.slf4j.Logger;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.util.ResourceUtils;
+
+
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+
+import net.shibboleth.shared.spring.httpclient.resource.ConnectionClosingInputStream;
+
+/**
+ * {@link Module} base class implementing basic file management.
+ * 
+ * @since 9.0.0
+ */
+public abstract class AbstractModule implements Module {
+
+    /** Extension for preserving user files. */
+    @Nonnull @NotEmpty private final String saveExt;
+
+    /** Base extension for adding new default files. */
+    @Nonnull @NotEmpty private final String baseExt;
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(AbstractModule.class);
+    
+    /** Module resources. */
+    @Nonnull private Collection<ModuleResource> moduleResources;
+    
+    /** Version-aware extension for new files added. */
+    @Nonnull private String idpNewExt;
+    
+    /**
+     * Constructor.
+     *
+     * @param version version of the containing application (or null if none available)
+     * @param basExt extension for adding new default files.
+     * @param savExt extension for preserving user files.
+     */
+    protected AbstractModule(@Nullable final String version, @Nonnull @NotEmpty final String basExt, @Nonnull @NotEmpty final String savExt) {
+        moduleResources = CollectionSupport.emptyList();
+        saveExt = savExt;
+        baseExt = basExt;
+        idpNewExt = version != null ? baseExt + "-" + version.replace(".", "") : baseExt;
+    }
+    
+    /** {@inheritDoc} */
+    @Nonnull @NotLive @Unmodifiable public Collection<ModuleResource> getResources() {
+        return 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 = CollectionSupport.copyToList(resources);
+        } else {
+            moduleResources = CollectionSupport.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) {
+            
+            if (resource.isOptional()) {
+                continue;
+            }
+
+            if (moduleContext.getInstallLocation().startsWith(ResourceUtils.CLASSPATH_URL_PREFIX)) {
+                final ClassPathResource cp;
+                if (moduleContext.getInstallLocation().equals(ResourceUtils.CLASSPATH_URL_PREFIX)) {
+                    cp = new ClassPathResource(resource.getDestination().toString());
+                } else {
+                    cp = (ClassPathResource) new ClassPathResource(
+                            moduleContext.getInstallLocation().substring(
+                                    ResourceUtils.CLASSPATH_URL_PREFIX.length())).createRelative(
+                                            resource.getDestination().toString());
+                }
+                
+                if (!cp.exists()) {
+                    log.debug("Module {}: resource destination {} missing, module is disabled", getId(),
+                            ResourceUtils.CLASSPATH_URL_PREFIX + cp.getPath());
+                    return false;
+                }
+            } else {
+                final Path resolved = Path.of(moduleContext.getInstallLocation()).resolve(resource.getDestination());
+                if (SystemUtils.IS_OS_WINDOWS && resource.isWindows() && !resolved.toFile().exists()) {
+                    log.debug("Module {}: resource destination {} missing, module is disabled", getId(), resolved);
+                    return false;
+                }
+                if (!SystemUtils.IS_OS_WINDOWS && resource.isNonWindows() && !resolved.toFile().exists()) {
+                    log.debug("Module {}: resource destination {} missing, module is disabled", getId(), resolved);
+                    return false;
+                }
+            }
+        }
+        
+        log.debug("Module {} is enabled", getId());
+        return true;
+    }
+    
+    /** {@inheritDoc} */
+    @Nonnull @NonnullElements public Map<ModuleResource, ResourceResult> enable(
+            @Nonnull final ModuleContext moduleContext) throws ModuleException {
+        
+        if (moduleContext.getInstallLocation().startsWith(ResourceUtils.CLASSPATH_URL_PREFIX)) {
+            throw new ModuleException("IdP location is a classpath");
+        }
+        
+        if (isHttpClientRequired() && moduleContext.getHttpClient() == null) {
+            throw new ModuleException("HTTP client required but not available");
+        }
+        
+        log.debug("Module {} enabling", getId());
+        
+        final Map<ModuleResource,ResourceResult> results;
+        
+        if (!moduleResources.isEmpty()) {
+            results = new LinkedHashMap<>(moduleResources.size());
+
+            for (final ModuleResource resource : moduleResources) {
+                if (SystemUtils.IS_OS_WINDOWS) {
+                    if (resource.isWindows()) {
+                        results.put(resource, ((BasicModuleResource) resource).enable(moduleContext));
+                    } else {
+                        log.debug("Module {}: skipping non-Windows resource {}", getId(), resource);
+                    }
+                } else {
+                    if (resource.isNonWindows()) {
+                        results.put(resource, ((BasicModuleResource) resource).enable(moduleContext));
+                    } else {
+                        log.debug("Module {}: skipping Windows resource {}", getId(), resource);
+                    }
+                }
+            }
+        } else {
+            results = CollectionSupport.emptyMap();
+        }
+        
+        log.debug("Module {} enabled", getId());
+        assert results != null;
+        return results;
+    }
+
+    /** {@inheritDoc} */
+    @Nonnull @NonnullElements public Map<ModuleResource, ResourceResult> disable(
+            @Nonnull final ModuleContext moduleContext, final boolean clean) throws ModuleException {
+
+        if (moduleContext.getInstallLocation().startsWith(ResourceUtils.CLASSPATH_URL_PREFIX)) {
+            throw new ModuleException("IdP location is a classpath");
+        }
+
+        log.debug("Module {} disabling", getId());
+
+        final Map<ModuleResource,ResourceResult> results;
+        
+        if (!moduleResources.isEmpty()) {
+            results = new LinkedHashMap<>(moduleResources.size());
+            for (final ModuleResource resource : moduleResources) {
+                if (SystemUtils.IS_OS_WINDOWS) {
+                    if (resource.isWindows()) {
+                        results.put(resource, ((BasicModuleResource) resource).disable(moduleContext, clean));
+                    }
+                } else {
+                    if (resource.isNonWindows()) {
+                        results.put(resource, ((BasicModuleResource) resource).disable(moduleContext, clean));
+                    }
+                }
+            }
+        } else {
+            results = CollectionSupport.emptyMap();
+        }
+        
+        log.debug("Module {} disabled", getId());
+        assert results != null;
+        return results;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean equals(final Object obj) {
+        return obj instanceof Module && Objects.equals(getId(), ((Module) obj).getId());
+    }
+
+
+    /** {@inheritDoc} */
+    @Override
+    public int hashCode() {
+        return Constraint.isNotNull(getId(), "ID cannot be null").hashCode();
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    public String toString() {
+        return "IdPModule " + getId();
+    }
+
+    /**
+     * Models a specific resource managed by a module.
+     */
+    protected class BasicModuleResource implements ModuleResource {
+        
+        /** Source. */
+        @Nonnull @NotEmpty private final String source;
+        
+        /** Destination. */
+        @Nonnull private final Path destination;
+        
+        /** Replacement criteria. */
+        private final boolean replace;
+
+        /** Optional criteria. */
+        private final boolean optional;
+
+        /** Executable criteria. */
+        private final boolean executable;
+        
+        /** Process on Windows? */
+        private final boolean windows;
+
+        /** Process on non-Windows? */
+        private final boolean nonwindows;
+
+        /**
+         * Constructor.
+         *
+         * @param src source
+         * @param dest destination
+         * @param shouldReplace whether to replace when enabling
+         * @param isOptional whether the resource is optional
+         * @param isExecutable whether the resource is executable
+         * @param isWindows whether the resource should be processed on Windows
+         * @param isNonWindows whether the resource should be processed on non-Windows platforms
+         */
+        public BasicModuleResource(@Nonnull @NotEmpty final String src, @Nonnull final Path dest,
+                final boolean shouldReplace, final boolean isOptional, final boolean isExecutable,
+                final boolean isWindows, final boolean isNonWindows) {
+            source = Constraint.isNotNull(StringSupport.trimOrNull(src), "Source cannot be null");
+            destination = Constraint.isNotNull(dest, "Destination cannot be null");
+            replace = shouldReplace;
+            optional = isOptional;
+            executable = isExecutable;
+            windows = isWindows;
+            nonwindows = isNonWindows;
+        }
+
+        /** {@inheritDoc} */
+        public int hashCode() {
+            return source.hashCode();
+        }
+
+        /** {@inheritDoc} */
+        public boolean equals(final Object obj) {
+            if (obj instanceof ModuleResource) {
+                return source.equals(((ModuleResource) obj).getSource()) &&
+                        destination.equals(((ModuleResource) obj).getDestination());
+            }
+            return false;
+        }
+
+        /** {@inheritDoc} */
+        @Nonnull public String getSource() {
+            return source;
+        }
+        
+        /** {@inheritDoc} */
+        @Nonnull public Path getDestination() {
+            return destination;
+        }
+        
+        /** {@inheritDoc} */
+        public boolean isReplace() {
+            return replace;
+        }
+
+        /** {@inheritDoc} */
+        public boolean isOptional() {
+            return optional;
+        }
+
+        /** {@inheritDoc} */
+        public boolean isExecutable() {
+            return executable;
+        }
+
+        /** {@inheritDoc} */
+        public boolean isWindows() {
+            return windows;
+        }
+
+        /** {@inheritDoc} */
+        public boolean isNonWindows() {
+            return nonwindows;
+        }
+
+        /**
+         * 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 !Arrays.equals(destHash, digest.digest());
+                            }
+                        }
+                        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://")) {
+                try {
+                    return connect(moduleContext, new URI(source));
+                } catch (final URISyntaxException e) {
+                    throw new IOException(e);
+                }
+            }
+            return AbstractModule.this.getClass().getResourceAsStream(source);
+        }
+
+        /**
+         * Connect to the given URI and return the HTTP response stream.
+         *
+         * @param moduleContext module context
+         * @param uri resource location
+         * 
+         * @return input stream of response
+         * 
+         * @throws IOException on errors
+         */
+        @Nonnull private InputStream connect(@Nonnull final ModuleContext moduleContext, @Nonnull final URI uri)
+                throws IOException {
+            
+            final HttpClientContext clientContext = HttpClientContext.create();
+            assert clientContext != null;
+            HttpClientSecuritySupport.marshalSecurityParameters(clientContext,
+                    moduleContext.getHttpClientSecurityParameters(), true);
+            ClassicHttpResponse response = null;
+            try {
+                log.debug("Module {} fetching HTTP resource {}", getId(), uri);
+                final HttpGet request = new HttpGet(uri);
+                response = Constraint.isNotNull(
+                        moduleContext.getHttpClient(), "HttpClient cannot be null").executeOpen(null, request, clientContext);
+                HttpClientSecuritySupport.checkTLSCredentialEvaluated(clientContext, request.getScheme());
+                if (response.getCode() != 200) {
+                    throw new IOException("HTTP request was unsuccessful");
+                }
+                
+                // The response socket should be closed after the stream is closed.
+                final InputStream ret = new ConnectionClosingInputStream(response);
+                response = null;
+                return ret;
+            } finally {
+                if (response != null && CloseableHttpResponse.class.isInstance(response)) {
+                    try {
+                        CloseableHttpResponse.class.cast(response).close();
+                    } catch (final IOException e) {
+                        log.debug("Error closing HttpResponse", e);
+                    }
+                }
+            }
+        }
+        
+        /**
+         * 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 Path destPath = Path.of(moduleContext.getInstallLocation()).resolve(destination);
+            if (Files.exists(destPath)) {
+                try {
+                    return Files.newInputStream(destPath, StandardOpenOption.READ);
+                } catch (final IOException e) {
+                    log.error("Module {} unable to read destination resource {}", getId(), destPath, e);
+                }
+            }
+            
+            return null;
+        }
+
+        /**
+         * Enable the supplied resource.
+         * 
+         * @param moduleContext module context
+         * 
+         * @return result of operation
+         * 
+         * @throws ModuleException if an error occurs
+         */
+        @Nonnull private ResourceResult 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 Path destPath;
+                final ResourceResult result;
+                
+                if (hasChanged) {
+                    if (isReplace()) {
+                        destPath = Path.of(moduleContext.getInstallLocation()).resolve(destination);
+                        final Path savedPath = destPath.resolveSibling(destPath.getFileName() + saveExt);
+                        if (savedPath.toFile().exists()) {
+                            throw new IOException(savedPath + " exists, aborting");
+                        }
+                        Files.copy(destPath, savedPath, StandardCopyOption.REPLACE_EXISTING);
+                        log.debug("Module {} preserved {}", getId(), destPath);
+                        result = ResourceResult.REPLACED;
+                    } else {
+                        final Path basePath = Path.of(moduleContext.getInstallLocation()).resolve(destination);
+                        destPath = basePath.resolveSibling(basePath.getFileName() + idpNewExt);
+                        result = ResourceResult.ADDED;
+                    }
+                    
+                } else {
+                    destPath = Path.of(moduleContext.getInstallLocation()).resolve(destination);
+                    result = ResourceResult.CREATED;
+                }
+                
+                if (!destPath.startsWith(moduleContext.getInstallLocation())) {
+                    log.error("Module {} attempted to create file outside of IdP installation: {}", getId(), destPath);
+                    throw new ModuleException("Module asked to create file outside of IdP installation");
+                }
+
+                try {
+                    Files.createDirectories(destPath.getParent());
+                } catch (final IOException e) {
+                    if (e instanceof FileAlreadyExistsException) {
+                        log.info("Path {} existed but not directory, assuming symlink", destPath.getParent());
+                    } else {
+                        throw e;
+                    }
+                }
+                Files.copy(srcStream, destPath, StandardCopyOption.REPLACE_EXISTING);
+                if (isExecutable()) {
+                    destPath.toFile().setExecutable(true);
+                }
+                log.debug("Module {} created {}", getId(), destPath);
+                return result;
+            } catch (final IOException e) {
+                log.error("Module {} unable to enable resource {}", getId(), source);
+                throw new ModuleException(e);
+            }
+        }
+
+        /**
+         * Disable the supplied resource, either removing or renaming.
+         * 
+         * @param moduleContext module context
+         * @param clean true iff resource should be removed
+         * 
+         * @return result of operation
+         * 
+         * @throws ModuleException if an error occurs
+         */
+        @Nonnull private ResourceResult disable(@Nonnull final ModuleContext moduleContext, final boolean clean)
+                throws ModuleException {
+            
+            final ResourceResult result;
+            final Path resolved = Path.of(moduleContext.getInstallLocation()).resolve(destination);
+            log.debug("Module {} resolved resource destination {}", getId(), resolved);
+            if (Files.exists(resolved)) {
+                try {
+                    if (clean || !hasChanged(moduleContext)) {
+                        log.debug("Module {} removing resource {}", getId(), resolved);
+                        Files.delete(resolved);
+                        result = ResourceResult.REMOVED;
+                    } else {
+                        log.debug("Module {} backing up resource {}", getId(), resolved);
+                        Files.move(resolved, resolved.resolveSibling(resolved.getFileName() + saveExt),
+                                StandardCopyOption.REPLACE_EXISTING);
+                        result = ResourceResult.SAVED;
+                    }
+                    final Path idpnewVersion = resolved.resolveSibling(resolved.getFileName() + idpNewExt);
+                    if (Files.exists(idpnewVersion)) {
+                        Files.delete(idpnewVersion);
+                    }
+                } catch (final IOException e) {
+                    log.error("Module {} failed to disable {}", getId(), resolved);
+                    throw new ModuleException(e);
+                }
+            } else {
+                log.debug("Module {} resource {} missing, ignoring", getId(), resolved);
+                result = ResourceResult.MISSING;
+            }
+            
+            return result;
+        }
+        
+    }
+
+}
\ No newline at end of file
diff --git a/shib-profile-api/src/main/java/net/shibboleth/profile/module/Module.java b/shib-profile-api/src/main/java/net/shibboleth/profile/module/Module.java
new file mode 100644
index 0000000..a77593f
--- /dev/null
+++ b/shib-profile-api/src/main/java/net/shibboleth/profile/module/Module.java
@@ -0,0 +1,208 @@
+/*
+ * 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.profile.module;
+
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.shared.component.IdentifiedComponent;
+
+/**
+ * This interface is the base of those exported via the service API into the UdP and
+ * in the future other applications.
+ * 
+ * @since 9.0.0
+ */
+public interface Module extends IdentifiedComponent {
+
+    /**
+     * Gets module name.
+     * 
+     * @param moduleContext optional context for supplying i18n input
+     * 
+     * @return a human-readable name for the module
+     */
+    @Nonnull @NotEmpty String getName(@Nullable final ModuleContext moduleContext);
+
+    /**
+     * Gets module description.
+     * 
+     * @param moduleContext optional context for supplying i18n input
+     * 
+     * @return a human-readable description for the module
+     */
+    @Nullable @NotEmpty String getDescription(@Nullable final ModuleContext moduleContext);
+
+    /**
+     * Gets module URL.
+     * 
+     * @return a URL for obtaining additional information about the module
+     */
+    @Nullable @NotEmpty String getURL();
+
+    /**
+     * Gets the plugin name of the module's owner, if the module is provided by a plugin.
+     *
+     * @return plugin ID, if any
+     */
+    @Nullable @NotEmpty String getOwnerId();
+
+    /**
+     * Gets whether module enablement requires access to an {@link org.apache.hc.client5.http.classic.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
+     *
+     * @return summary of resource results
+     *
+     * @throws ModuleException if not successful 
+     */
+    @Nonnull @NonnullElements Map<ModuleResource,ResourceResult> 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
+     * 
+     * @return summary of resource results
+     *
+     * @throws ModuleException if not successful 
+     */
+    @Nonnull @NonnullElements Map<ModuleResource,ResourceResult> disable(@Nonnull final ModuleContext moduleContext,
+            final boolean clean) throws ModuleException;
+
+    /**
+     * Interface to a resource managed by the module.
+     */
+    public 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();
+
+        /**
+         * Gets whether the resource, if missing, should not act as a module-disabled signal.
+         *
+         * @return true iff the resource may be removed by a deployer without disabling the module
+         */
+        public boolean isOptional();
+
+        /**
+         * Gets whether the resource should be marked executable where applicable.
+         *
+         * @return true iff the resource should be marked executable
+         */
+        public boolean isExecutable();
+
+        /**
+         * Gets whether the resource should be processed on Windows.
+         *
+         * @return true iff the resource should be processed on Windows
+         *
+         */
+        public boolean isWindows();
+
+        /**
+         * Gets whether the resource should be processed on non-Windows platforms.
+         *
+         * @return true iff the resource should be processed on non-Windows platforms
+         */
+        public boolean isNonWindows();
+    }
+
+    /** Resource management outcome. */
+    public enum ResourceResult {
+        /** Resource was created. */
+        CREATED,
+
+        /** Resource was created and old resource saved. */
+        REPLACED,
+
+        /** Resource was created alongside original. */
+        ADDED,
+
+        /** Resource was removed. */
+        REMOVED,
+
+        /** Old resource was preserved. */
+        SAVED,
+
+        /** Resource was missing. */
+        MISSING,
+    };
+ }
diff --git a/shib-profile-api/src/main/java/net/shibboleth/profile/module/ModuleContext.java b/shib-profile-api/src/main/java/net/shibboleth/profile/module/ModuleContext.java
new file mode 100644
index 0000000..46b85f3
--- /dev/null
+++ b/shib-profile-api/src/main/java/net/shibboleth/profile/module/ModuleContext.java
@@ -0,0 +1,157 @@
+/*
+ * 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.profile.module;
+
+import java.io.PrintStream;
+import java.util.List;
+import java.util.Locale.LanguageRange;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.apache.hc.client5.http.classic.HttpClient;
+import org.opensaml.security.httpclient.HttpClientSecurityParameters;
+
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * Information required to perform some module operations.
+ * 
+ * @since 9.0.0
+ */
+public class ModuleContext {
+
+    /** Installation root. */
+    @Nonnull private String installationHome;
+
+    /** HttpClient if needed. */
+    @Nullable private HttpClient httpClient;
+    
+    /** HTTP security parameters. */
+    @Nullable private HttpClientSecurityParameters httpClientSecurityParams;
+    
+    /** Language expressions to use for i18n. */
+    @Nonnull @NonnullElements private List<LanguageRange> languageRanges;
+    
+    /** Output stream for sending output to the module consumer. */
+    @Nullable private PrintStream messageStream;
+
+    /**
+     * Constructor.
+     *
+     * @param home location of IdP install
+     */
+    public ModuleContext(@Nonnull @NotEmpty final String home) {
+        installationHome = Constraint.isNotEmpty(home, "Home location cannot be null or empty");
+        languageRanges = CollectionSupport.emptyList();
+    }
+
+    /**
+     * Gets software installation location.
+     * 
+     * @return install path
+     *
+     * @since 4.2.0
+     */
+    @Nonnull @NotEmpty public String getInstallLocation() {
+        return installationHome;
+    }
+
+    /**
+     * Gets an {@link HttpClient} instance to use if available.
+     *
+     * @return HTTP client instance
+     */
+    @Nullable public HttpClient getHttpClient() {
+        return httpClient;
+    }
+
+    /**
+     * Sets an {@link HttpClient} instance to use.
+     *
+     * @param client client to use
+     */
+    public void setHttpClient(@Nullable final HttpClient client) {
+        httpClient = client;
+    }
+
+    /**
+     * Gets {@link HttpClient} security parameters, if any.
+     *
+     * @return HTTP client security parameters to use
+     */
+    @Nullable public HttpClientSecurityParameters getHttpClientSecurityParameters() {
+        return httpClientSecurityParams;
+    }
+
+    /**
+     * Sets {@link HttpClient} security parameters to use.
+     *
+     * @param params security parameters
+     */
+    public void setHttpClientSecurityParameters(@Nullable final HttpClientSecurityParameters params) {
+        httpClientSecurityParams = params;
+    }
+
+    /**
+     * Gets the language ranges to use for i18n.
+     *
+     * @return language ranges
+     */
+    @Nonnull @NonnullElements @NotLive @Unmodifiable public List<LanguageRange> getLanguageRanges() {
+        return languageRanges;
+    }
+
+    /**
+     * Sets the language ranges to use for i18n.
+     *
+     * @param ranges language ranges
+     */
+    public void setLanguageRanges(@Nullable @NonnullElements final List<LanguageRange> ranges) {
+        if (ranges != null) {
+            languageRanges = CollectionSupport.copyToList(ranges);
+        } else {
+            languageRanges = CollectionSupport.emptyList();
+        }
+    }
+
+    /**
+     * Gets the output stream to receive any instructions or additional information after
+     * performing operations.
+     *
+     * @return output stream, or null
+     */
+    @Nullable public PrintStream getMessageStream() {
+        return messageStream;
+    }
+
+    /**
+     * Sets the output stream to receive any instructions or additional information after
+     * performing operations.
+     *
+     * @param stream output stream
+     */
+    public void setMessageStream(@Nullable final PrintStream stream) {
+        messageStream = stream;
+    }
+}
\ No newline at end of file
diff --git a/shib-profile-api/src/main/java/net/shibboleth/profile/module/ModuleException.java b/shib-profile-api/src/main/java/net/shibboleth/profile/module/ModuleException.java
new file mode 100644
index 0000000..3698a5a
--- /dev/null
+++ b/shib-profile-api/src/main/java/net/shibboleth/profile/module/ModuleException.java
@@ -0,0 +1,66 @@
+/*
+ * 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.profile.module;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * Module exception class.
+ * 
+ * @since 9.0.0
+ */
+ at ThreadSafe
+public class ModuleException extends Exception {
+
+    /** Serial Number. */
+    private static final long serialVersionUID = -7752907205901300896L;
+
+    /** 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/shib-profile-api/src/main/java/net/shibboleth/profile/module/package-info.java b/shib-profile-api/src/main/java/net/shibboleth/profile/module/package-info.java
new file mode 100644
index 0000000..6d55837
--- /dev/null
+++ b/shib-profile-api/src/main/java/net/shibboleth/profile/module/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+/** 
+ * Classes for Modules for the IdP and eventually other applications.
+ */
+ at NonnullElements
+package net.shibboleth.profile.module;
+
+import net.shibboleth.shared.annotation.constraint.NonnullElements;

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


More information about the commits mailing list