[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