[java-identity-provider] branch main updated: IDP-1664 - Support Module service API
Scott Cantor
cantor.2 at osu.edu
Wed Sep 2 15:42:18 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=ef9b69d817dc10fa712625e84d119326e650bcd0
The following commit(s) were added to refs/heads/main by this push:
new ef9b69d81 IDP-1664 - Support Module service API
ef9b69d81 is described below
commit ef9b69d817dc10fa712625e84d119326e650bcd0
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Wed Sep 2 11:41:53 2020 -0400
IDP-1664 - Support Module service API
https://issues.shibboleth.net/jira/browse/IDP-1664
Redo file handling code and fix some bugs.
First batch of unit tests.
---
.../shibboleth/idp/module/AbstractIdPModule.java | 66 ++++----
.../java/net/shibboleth/idp/module/IdPModule.java | 28 ----
.../net/shibboleth/idp/module/ModuleContext.java | 98 +++++++++++
.../idp/module/PropertyDrivenIdPModule.java | 3 +-
.../net/shibboleth/idp/module/IdPModuleTest.java | 181 ++++++++++++++++++---
idp-admin-api/src/test/resources/logback-test.xml | 2 +-
.../net/shibboleth/idp/module/module.properties | 9 +-
.../resources/net/shibboleth/idp/module/test.vm | 1 +
.../resources/net/shibboleth/idp/module/test.xml | 1 +
9 files changed, 303 insertions(+), 86 deletions(-)
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
index 9a68ab59e..2883d073a 100644
--- 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
@@ -19,14 +19,16 @@ 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.Files;
import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@@ -191,7 +193,7 @@ public abstract class AbstractIdPModule implements IdPModule {
try (final OutputStream srcSink = OutputStream.nullOutputStream();
final DigestOutputStream srcDigest = new DigestOutputStream(srcSink, digest)) {
src.transferTo(srcDigest);
- return digest.digest().equals(destHash);
+ return !Arrays.equals(destHash, digest.digest());
}
}
log.debug("Module {} resource {} does not exist at source", getId(), source);
@@ -241,9 +243,10 @@ public abstract class AbstractIdPModule implements IdPModule {
@Nullable private InputStream getDestinationStream(@Nonnull final ModuleContext moduleContext)
throws IOException {
final File destFile = moduleContext.getIdPHome().resolve(destination).toFile();
- if (destFile.exists()) {
+ if (destFile.exists() && destFile.isFile() && destFile.canRead()) {
return new FileInputStream(destFile);
}
+
return null;
}
@@ -264,33 +267,33 @@ public abstract class AbstractIdPModule implements IdPModule {
throw new IOException("Source stream was null");
}
-
- final File destFile;
+ final Path destPath;
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();
+ destPath = moduleContext.getIdPHome().resolve(destination);
+ Files.copy(destPath, destPath.resolveSibling(destPath.getFileName() + ".idpsave"),
+ StandardCopyOption.REPLACE_EXISTING);
+ log.info("Module {} preserved {}", getId(), destPath);
} else {
- destFile = new File(moduleContext.getIdPHome().resolve(destination).toString() + ".idpnew");
+ final Path basePath = moduleContext.getIdPHome().resolve(destination);
+ destPath = basePath.resolveSibling(basePath.getFileName() + ".idpnew");
}
} else {
- destFile = moduleContext.getIdPHome().resolve(destination).toFile();
+ destPath = moduleContext.getIdPHome().resolve(destination);
}
-
- try (final OutputStream destStream = new FileOutputStream(destFile)) {
- srcStream.transferTo(destStream);
- log.info("Module {} created {}", getId(), destFile);
+
+ if (!destPath.startsWith(moduleContext.getIdPHome())) {
+ 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");
}
+ Files.copy(srcStream, destPath, StandardCopyOption.REPLACE_EXISTING);
+ log.info("Module {} created {}", getId(), destPath);
+
} catch (final IOException e) {
- log.error("Module {} unable to enable resource {}", getId(), source, e);
+ log.error("Module {} unable to enable resource {}", getId(), source);
throw new ModuleException(e);
}
}
@@ -307,21 +310,22 @@ public abstract class AbstractIdPModule implements IdPModule {
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);
+ if (Files.exists(resolved)) {
+ try {
+ if (clean) {
+ log.info("Module {} removing resource {}", getId(), resolved);
+ Files.delete(resolved);
+ } else {
+ log.info("Module {} backing up resource {}", getId(), resolved);
+ Files.move(resolved, resolved.resolveSibling(resolved.getFileName() + ".idpsave"),
+ StandardCopyOption.REPLACE_EXISTING);
}
+ } catch (final IOException e) {
+ log.error("Module {} failed to disable {}", getId(), resolved);
+ throw new ModuleException(e);
}
} else {
- log.info("Module {} resource {} missing, ignoring", getId(), file);
+ log.info("Module {} resource {} missing, ignoring", getId(), resolved);
}
}
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
index b89dbc3c7..4ef10f257 100644
--- 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
@@ -25,7 +25,6 @@ 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;
@@ -109,33 +108,6 @@ public interface IdPModule extends IdentifiedComponent {
* @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.
diff --git a/idp-admin-api/src/main/java/net/shibboleth/idp/module/ModuleContext.java b/idp-admin-api/src/main/java/net/shibboleth/idp/module/ModuleContext.java
new file mode 100644
index 000000000..69c031a17
--- /dev/null
+++ b/idp-admin-api/src/main/java/net/shibboleth/idp/module/ModuleContext.java
@@ -0,0 +1,98 @@
+/*
+ * 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.nio.file.Path;
+
+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.logic.Constraint;
+
+/**
+ * Information required to perform some module operations.
+ */
+public final class ModuleContext {
+
+ /** IdP installation root. */
+ @Nonnull private Path idpHome;
+
+ /** HttpClient if needed. */
+ @Nullable private HttpClient httpClient;
+
+ /** HTTP security parameters. */
+ @Nullable private HttpClientSecurityParameters httpClientSecurityParams;
+
+ /**
+ * Constructor.
+ *
+ * @param home location of IdP install
+ */
+ public ModuleContext(@Nonnull final Path home) {
+ idpHome = Constraint.isNotNull(home, "IdP home path cannot be null");
+ }
+
+ /**
+ * Gets software installation location.
+ *
+ * @return install path
+ */
+ @Nonnull Path getIdPHome() {
+ return idpHome;
+ }
+
+ /**
+ * Gets an {@link HttpClient} instance to use if available.
+ *
+ * @return HTTP client instance
+ */
+ @Nullable 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 HttpClientSecurityParameters getHttpClientSecurityParameters() {
+ return httpClientSecurityParams;
+ }
+
+ /**
+ * Sets {@link HttpClient} security parameters to use.
+ *
+ * @param params security parameters
+ */
+ public void setHttpClientSecurityParameters(@Nullable final HttpClientSecurityParameters params) {
+ httpClientSecurityParams = params;
+ }
+
+}
\ 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
index 79e76f070..4fd2fd17c 100644
--- 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
@@ -148,8 +148,7 @@ public class PropertyDrivenIdPModule extends AbstractIdPModule {
moduleProperties.getProperty(getId() + renumstr + MODULE_REPLACE_PROPERTY, "false"));
final Path destPath = Path.of(dest);
- if (dest.contains("..") || destPath.isAbsolute() || destPath.startsWith("/") ||
- destPath.startsWith("\\")) {
+ if (dest.contains("..") || destPath.isAbsolute() || destPath.startsWith("/")) {
throw new ModuleException("Module contained a suspect resource destination");
}
diff --git a/idp-admin-api/src/test/java/net/shibboleth/idp/module/IdPModuleTest.java b/idp-admin-api/src/test/java/net/shibboleth/idp/module/IdPModuleTest.java
index b267e4393..dbdffd0b7 100644
--- a/idp-admin-api/src/test/java/net/shibboleth/idp/module/IdPModuleTest.java
+++ b/idp-admin-api/src/test/java/net/shibboleth/idp/module/IdPModuleTest.java
@@ -16,7 +16,13 @@ package net.shibboleth.idp.module;
* limitations under the License.
*/
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
import java.util.Iterator;
import java.util.Optional;
import java.util.ServiceConfigurationError;
@@ -24,6 +30,8 @@ import java.util.ServiceLoader;
import java.util.ServiceLoader.Provider;
import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import net.shibboleth.idp.module.IdPModule.ModuleResource;
@@ -33,33 +41,55 @@ import net.shibboleth.idp.module.IdPModule.ModuleResource;
*/
public class IdPModuleTest {
- @Test
- public void testModule() {
-
+ private static final String XML_DATA = "<test>foo</test>\n";
+ private static final String XML_OTHER_DATA = "<test>bar</test>\n";
+ private static final String VEL_DATA = "## something\n";
+ private static final String VEL_OTHER_DATA = "## something else\n";
+
+ private Path testHome;
+ private IdPModule testModule;
+ private ModuleContext context;
+
+ @BeforeMethod
+ public void setUp() throws IOException {
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();
+ testModule = 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/test.xml");
- Assert.assertEquals(resource.getDestination(), Path.of("conf/test.xml"));
-
- resource = resources.next();
- Assert.assertEquals(resource.getSource(), "net/shibboleth/idp/module/test.vm");
- Assert.assertEquals(resource.getDestination(), Path.of("views/test.vm"));
+ testHome = Files.createTempDirectory("test-idp-home-");
+ context = new ModuleContext(testHome);
}
-
+
+ @AfterMethod
+ public void tearDown() throws IOException {
+ if (testHome != null) {
+ Files.walkFileTree(testHome, new SimpleFileVisitor<Path>() {
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
+ throws IOException
+ {
+ Files.delete(file);
+ return FileVisitResult.CONTINUE;
+ }
+ @Override
+ public FileVisitResult postVisitDirectory(Path dir, IOException e)
+ throws IOException
+ {
+ if (e == null) {
+ Files.delete(dir);
+ return FileVisitResult.CONTINUE;
+ }
+ // directory iteration failed
+ throw e;
+ }
+ });
+ testHome = null;
+ }
+ }
+
@Test
public void testBadModules() {
@@ -84,5 +114,116 @@ public class IdPModuleTest {
Assert.assertTrue(e.getCause() instanceof ModuleException);
}
}
+
+ @Test
+ public void testModule() {
+
+ Assert.assertEquals(testModule.getId(), TestModule.class.getName());
+ Assert.assertEquals(testModule.getName(), "Test module");
+ Assert.assertEquals(testModule.getURL().toString(), "https://wiki.shibboleth.net/confluence/display/IDP4/Home");
+
+ final Iterator<ModuleResource> resources = testModule.getResources().iterator();
+ Assert.assertEquals(testModule.getResources().size(), 2);
+
+ ModuleResource resource = resources.next();
+ Assert.assertEquals(resource.getSource(), "/net/shibboleth/idp/module/test.xml");
+ Assert.assertEquals(resource.getDestination(), Path.of("conf/test.xml"));
+
+ resource = resources.next();
+ Assert.assertEquals(resource.getSource(), "/net/shibboleth/idp/module/test.vm");
+ Assert.assertEquals(resource.getDestination(), Path.of("views/test.vm"));
+ }
+
+ @Test(expectedExceptions=ModuleException.class)
+ public void testEnableNoTree() throws ModuleException {
+ testModule.enable(context);
+ }
+
+ @Test
+ public void testDisableNoTree() throws ModuleException {
+ testModule.disable(context, true);
+ testModule.disable(context, false);
+ }
+ @Test
+ public void testEnableClean() throws ModuleException, IOException {
+
+ Files.createDirectory(testHome.resolve("conf"));
+ Files.createDirectory(testHome.resolve("views"));
+
+ testModule.enable(context);
+
+ String xml = Files.readString(testHome.resolve("conf/test.xml"));
+ Assert.assertEquals(xml, XML_DATA);
+
+ String vel = Files.readString(testHome.resolve("views/test.vm"));
+ Assert.assertEquals(vel, VEL_DATA);
+
+ testModule.disable(context, true);
+ Assert.assertEquals(testHome.resolve("conf").toFile().listFiles().length, 0);
+ Assert.assertEquals(testHome.resolve("views").toFile().listFiles().length, 0);
+
+ testModule.enable(context);
+ testModule.disable(context, false);
+ xml = Files.readString(testHome.resolve("conf/test.xml.idpsave"));
+ Assert.assertEquals(xml, XML_DATA);
+
+ vel = Files.readString(testHome.resolve("views/test.vm.idpsave"));
+ Assert.assertEquals(vel, VEL_DATA);
+ }
+
+ @Test
+ public void testEnableExistingSame() throws IOException, ModuleException {
+ Files.createDirectory(testHome.resolve("conf"));
+ Files.createDirectory(testHome.resolve("views"));
+
+ try (final OutputStream os =
+ Files.newOutputStream(Files.createFile(testHome.resolve("conf/test.xml")))) {
+ os.write(XML_DATA.getBytes());
+ }
+
+ try (final OutputStream os =
+ Files.newOutputStream(Files.createFile(testHome.resolve("views/test.vm")))) {
+ os.write(VEL_DATA.getBytes());
+ }
+
+ testModule.enable(context);
+
+ String xml = Files.readString(testHome.resolve("conf/test.xml"));
+ Assert.assertEquals(xml, XML_DATA);
+ Assert.assertFalse(testHome.resolve("conf/test.xml.idpsave").toFile().exists());
+
+ String vel = Files.readString(testHome.resolve("views/test.vm"));
+ Assert.assertEquals(vel, VEL_DATA);
+ Assert.assertFalse(testHome.resolve("views/test.vm.idpnew").toFile().exists());
+ }
+
+ @Test
+ public void testEnableExistingDifferent() throws IOException, ModuleException {
+ Files.createDirectory(testHome.resolve("conf"));
+ Files.createDirectory(testHome.resolve("views"));
+
+ try (final OutputStream os =
+ Files.newOutputStream(Files.createFile(testHome.resolve("conf/test.xml")))) {
+ os.write(XML_OTHER_DATA.getBytes());
+ }
+
+ try (final OutputStream os =
+ Files.newOutputStream(Files.createFile(testHome.resolve("views/test.vm")))) {
+ os.write(VEL_OTHER_DATA.getBytes());
+ }
+
+ testModule.enable(context);
+
+ String xml = Files.readString(testHome.resolve("conf/test.xml"));
+ Assert.assertEquals(xml, XML_DATA);
+ xml = Files.readString(testHome.resolve("conf/test.xml.idpsave"));
+ Assert.assertEquals(xml, XML_OTHER_DATA);
+
+ String vel = Files.readString(testHome.resolve("views/test.vm"));
+ Assert.assertEquals(vel, VEL_OTHER_DATA);
+ vel = Files.readString(testHome.resolve("views/test.vm.idpnew"));
+ Assert.assertEquals(vel, VEL_DATA);
+ }
+
}
\ No newline at end of file
diff --git a/idp-admin-api/src/test/resources/logback-test.xml b/idp-admin-api/src/test/resources/logback-test.xml
index f3280e56b..8f891b483 100644
--- a/idp-admin-api/src/test/resources/logback-test.xml
+++ b/idp-admin-api/src/test/resources/logback-test.xml
@@ -10,7 +10,7 @@
</appender>
<root>
- <level value="warn" />
+ <level value="DEBUG" />
<appender-ref ref="STDOUT" />
</root>
diff --git a/idp-admin-api/src/test/resources/net/shibboleth/idp/module/module.properties b/idp-admin-api/src/test/resources/net/shibboleth/idp/module/module.properties
index 50a9c3209..ad2b75892 100644
--- a/idp-admin-api/src/test/resources/net/shibboleth/idp/module/module.properties
+++ b/idp-admin-api/src/test/resources/net/shibboleth/idp/module/module.properties
@@ -6,20 +6,21 @@ net.shibboleth.idp.module.TestModule.name = Test module
net.shibboleth.idp.module.TestModule.desc = Module for unit tests
net.shibboleth.idp.module.TestModule.url = https://wiki.shibboleth.net/confluence/display/IDP4/Home
-net.shibboleth.idp.module.TestModule.1.src = net/shibboleth/idp/module/test.xml
+net.shibboleth.idp.module.TestModule.1.src = /net/shibboleth/idp/module/test.xml
net.shibboleth.idp.module.TestModule.1.dest = conf/test.xml
+net.shibboleth.idp.module.TestModule.1.replace = true
-net.shibboleth.idp.module.TestModule.2.src = net/shibboleth/idp/module/test.vm
+net.shibboleth.idp.module.TestModule.2.src = /net/shibboleth/idp/module/test.vm
net.shibboleth.idp.module.TestModule.2.dest = views/test.vm
# Broken modules due to dangerous resources
net.shibboleth.idp.module.BadModule.name = Bad module
net.shibboleth.idp.module.BadModule.desc = Module for unit tests with error
-net.shibboleth.idp.module.BadModule.1.src = net/shibboleth/idp/module/test.xml
+net.shibboleth.idp.module.BadModule.1.src = /net/shibboleth/idp/module/test.xml
net.shibboleth.idp.module.BadModule.1.dest = ../conf/test.xml
net.shibboleth.idp.module.BadModule2.name = Bad module 2
net.shibboleth.idp.module.BadModule2.desc = Module for unit tests with error
-net.shibboleth.idp.module.BadModule2.1.src = net/shibboleth/idp/module/test.xml
+net.shibboleth.idp.module.BadModule2.1.src = /net/shibboleth/idp/module/test.xml
net.shibboleth.idp.module.BadModule2.1.dest = /conf/test.xml
diff --git a/idp-admin-api/src/test/resources/net/shibboleth/idp/module/test.vm b/idp-admin-api/src/test/resources/net/shibboleth/idp/module/test.vm
new file mode 100644
index 000000000..239c69ea1
--- /dev/null
+++ b/idp-admin-api/src/test/resources/net/shibboleth/idp/module/test.vm
@@ -0,0 +1 @@
+## something
diff --git a/idp-admin-api/src/test/resources/net/shibboleth/idp/module/test.xml b/idp-admin-api/src/test/resources/net/shibboleth/idp/module/test.xml
new file mode 100644
index 000000000..a544c98ec
--- /dev/null
+++ b/idp-admin-api/src/test/resources/net/shibboleth/idp/module/test.xml
@@ -0,0 +1 @@
+<test>foo</test>
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list