[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