[java-idp-plugin-metadatagen] branch main updated: JMETAGEN-5 - Metadata generation

Scott Cantor cantor.2 at osu.edu
Tue Jul 18 16:10:41 UTC 2023


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

scantor pushed a commit to branch main
in repository java-idp-plugin-metadatagen.

View the commit online:
http://git.shibboleth.net/view/?p=java-idp-plugin-metadatagen.git;a=commit;h=b1028d03350e161692c44102a1ff2889b7c5133f

The following commit(s) were added to refs/heads/main by this push:
     new b1028d0  JMETAGEN-5 - Metadata generation
b1028d0 is described below

commit b1028d03350e161692c44102a1ff2889b7c5133f
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Tue Jul 18 12:10:38 2023 -0400

    JMETAGEN-5 - Metadata generation
    
    https://shibboleth.atlassian.net/browse/JMETAGEN-5
    
    Adjust module name.
    Add layout template feature to auto-populate endpoints.
    Bug fixes.
---
 metadatagen-impl/pom.xml                           |  43 ++-
 .../plugin/metadatagen/impl/MetadataGenCLI.java    |   4 +
 .../impl/MetadataGenCommandLineArguments.java      | 378 +++++++++++++++------
 .../metadatagen/conf/examples/shibmd-layout.xml    |  51 +++
 .../idp/plugin/metadatagen/conf/velocity.xml       |  52 +--
 .../idp/plugin/metadatagen/impl/module.properties  |  29 +-
 .../plugin/metadatagen/impl/MetadataGenTest.java   |  18 +
 .../idp/plugin/metadatagen/impl/batch.txt          |   2 +-
 8 files changed, 427 insertions(+), 150 deletions(-)

diff --git a/metadatagen-impl/pom.xml b/metadatagen-impl/pom.xml
index dc0b8a8..76f62c7 100644
--- a/metadatagen-impl/pom.xml
+++ b/metadatagen-impl/pom.xml
@@ -49,34 +49,33 @@
 
         <dependency>
             <groupId>${shib-shared.groupId}</groupId>
-            <artifactId>shib-profile-impl</artifactId>
+            <artifactId>shib-metadata-api</artifactId>
             <scope>provided</scope>
         </dependency>
 
         <dependency>
-            <groupId>${shib-shared.groupId}</groupId>
-            <artifactId>shib-metadata-api</artifactId>
+            <groupId>${opensaml.groupId}</groupId>
+            <artifactId>opensaml-core-api</artifactId>
             <scope>provided</scope>
         </dependency>
-
         <dependency>
             <groupId>${opensaml.groupId}</groupId>
-            <artifactId>opensaml-saml-api</artifactId>
+            <artifactId>opensaml-core-impl</artifactId>
             <scope>provided</scope>
         </dependency>
         <dependency>
             <groupId>${opensaml.groupId}</groupId>
-            <artifactId>opensaml-saml-impl</artifactId>
+            <artifactId>opensaml-saml-api</artifactId>
             <scope>provided</scope>
         </dependency>
         <dependency>
             <groupId>${opensaml.groupId}</groupId>
-            <artifactId>opensaml-security-api</artifactId>
+            <artifactId>opensaml-saml-impl</artifactId>
             <scope>provided</scope>
         </dependency>
         <dependency>
             <groupId>${opensaml.groupId}</groupId>
-            <artifactId>opensaml-xmlsec-api</artifactId>
+            <artifactId>opensaml-security-api</artifactId>
             <scope>provided</scope>
         </dependency>
 
@@ -95,6 +94,22 @@
             <artifactId>shib-velocity</artifactId>
             <scope>provided</scope>
         </dependency>
+
+        <dependency>
+            <groupId>${spring.groupId}</groupId>
+            <artifactId>spring-beans</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>${spring.groupId}</groupId>
+            <artifactId>spring-context</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>${spring.groupId}</groupId>
+            <artifactId>spring-core</artifactId>
+            <scope>provided</scope>
+        </dependency>
     
         <dependency>
             <groupId>org.apache.velocity</groupId>
@@ -102,6 +117,18 @@
             <scope>provided</scope>
         </dependency>
 
+        <dependency>
+            <groupId>com.beust</groupId>
+            <artifactId>jcommander</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
         <!-- Test dependencies. -->
         <dependency>
             <groupId>${idp.groupId}</groupId>
diff --git a/metadatagen-impl/src/main/java/net/shibboleth/idp/plugin/metadatagen/impl/MetadataGenCLI.java b/metadatagen-impl/src/main/java/net/shibboleth/idp/plugin/metadatagen/impl/MetadataGenCLI.java
index 7cea70d..de767c0 100644
--- a/metadatagen-impl/src/main/java/net/shibboleth/idp/plugin/metadatagen/impl/MetadataGenCLI.java
+++ b/metadatagen-impl/src/main/java/net/shibboleth/idp/plugin/metadatagen/impl/MetadataGenCLI.java
@@ -201,6 +201,8 @@ public final class MetadataGenCLI extends AbstractIdPHomeAwareCommandLine<Metada
                         jc.parse(args.toArray(new String[args.size()]));
                         batchList.add(argObject);
                         args.clear();
+                    } else if (line.isBlank()) {
+                        continue;
                     } else {
                         addBatchArguments(args, line);
                     }
@@ -234,10 +236,12 @@ public final class MetadataGenCLI extends AbstractIdPHomeAwareCommandLine<Metada
 
             if (batchList.isEmpty()) {
                 assert args != null;
+                args.loadLayoutObject();
                 generator.generate(args, sink);
             } else {
                 sink.write("<md:EntitiesDescriptor xmlns:md=\"urn:oasis:names:tc:SAML:2.0:metadata\" xmlns:mdattr=\"urn:oasis:names:tc:SAML:metadata:attribute\" xmlns:mdui=\"urn:oasis:names:tc:SAML:metadata:ui\" xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" xmlns:shibmd=\"urn:mace:shibboleth:metadata:1.0\" xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\">\n\n");
                 for (final MetadataGenCommandLineArguments batchEntry : batchList) {
+                    batchEntry.loadLayoutObject();
                     generator.generate(batchEntry, sink);
                 }
                 sink.write("\n</md:EntitiesDescriptor>\n");
diff --git a/metadatagen-impl/src/main/java/net/shibboleth/idp/plugin/metadatagen/impl/MetadataGenCommandLineArguments.java b/metadatagen-impl/src/main/java/net/shibboleth/idp/plugin/metadatagen/impl/MetadataGenCommandLineArguments.java
index 6716e88..59e7e7e 100644
--- a/metadatagen-impl/src/main/java/net/shibboleth/idp/plugin/metadatagen/impl/MetadataGenCommandLineArguments.java
+++ b/metadatagen-impl/src/main/java/net/shibboleth/idp/plugin/metadatagen/impl/MetadataGenCommandLineArguments.java
@@ -17,6 +17,9 @@
 package net.shibboleth.idp.plugin.metadatagen.impl;
 
 import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
 import java.io.PrintStream;
 import java.security.cert.CertificateException;
 import java.security.cert.X509Certificate;
@@ -33,8 +36,12 @@ import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
 import org.opensaml.core.xml.Namespace;
+import org.opensaml.core.xml.XMLObject;
 import org.opensaml.core.xml.XMLObjectBuilderFactory;
 import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
+import org.opensaml.core.xml.io.MarshallingException;
+import org.opensaml.core.xml.io.UnmarshallingException;
+import org.opensaml.core.xml.util.XMLObjectSupport;
 import org.opensaml.saml.common.SAMLObjectBuilder;
 import org.opensaml.saml.common.xml.SAMLConstants;
 import org.opensaml.saml.ext.saml2mdui.Logo;
@@ -42,16 +49,23 @@ import org.opensaml.saml.metadata.generator.impl.ArtifactResolutionServiceConver
 import org.opensaml.saml.metadata.generator.impl.AttributeServiceConverter;
 import org.opensaml.saml.metadata.generator.impl.SingleSignOnServiceConverter;
 import org.opensaml.saml.metadata.generator.impl.TemplateMetadataGeneratorParameters;
+import org.opensaml.saml.saml2.metadata.ArtifactResolutionService;
+import org.opensaml.saml.saml2.metadata.AssertionConsumerService;
 import org.opensaml.saml.saml2.metadata.AttributeAuthorityDescriptor;
+import org.opensaml.saml.saml2.metadata.AttributeService;
 import org.opensaml.saml.saml2.metadata.ContactPerson;
 import org.opensaml.saml.saml2.metadata.ContactPersonTypeEnumeration;
 import org.opensaml.saml.saml2.metadata.EmailAddress;
 import org.opensaml.saml.saml2.metadata.Endpoint;
+import org.opensaml.saml.saml2.metadata.EntityDescriptor;
 import org.opensaml.saml.saml2.metadata.GivenName;
 import org.opensaml.saml.saml2.metadata.IDPSSODescriptor;
 import org.opensaml.saml.saml2.metadata.IndexedEndpoint;
 import org.opensaml.saml.saml2.metadata.NameIDFormat;
+import org.opensaml.saml.saml2.metadata.RoleDescriptor;
 import org.opensaml.saml.saml2.metadata.SPSSODescriptor;
+import org.opensaml.saml.saml2.metadata.SingleLogoutService;
+import org.opensaml.saml.saml2.metadata.SingleSignOnService;
 import org.opensaml.saml.saml2.metadata.SurName;
 import org.opensaml.security.x509.X509Support;
 import org.slf4j.Logger;
@@ -71,6 +85,8 @@ import net.shibboleth.shared.codec.Base64Support;
 import net.shibboleth.shared.codec.EncodingException;
 import net.shibboleth.shared.collection.CollectionSupport;
 import net.shibboleth.shared.primitive.StringSupport;
+import net.shibboleth.shared.xml.ParserPool;
+import net.shibboleth.shared.xml.XMLParserException;
 
 /**
  * Command line arguments for Metadata Generation.
@@ -84,6 +100,9 @@ public class MetadataGenCommandLineArguments extends AbstractIdPHomeAwareCommand
     /** Logger. */
     @Nullable private Logger log;
     
+    /** Endpoint layout template object */
+    @Nullable private XMLObject layoutObject;
+    
     /** Cached IdP role. */
     @Nullable private IDPSSODescriptor idpRole;
 
@@ -201,7 +220,7 @@ public class MetadataGenCommandLineArguments extends AbstractIdPHomeAwareCommand
     
     /** Tag assignment expressions. */
     @Parameter(names = {"--entity-attr", "--tag"}, description="EntityAttribute tag=value expression")
-    @Nonnull private List<String> tagAssignments = new ArrayList();
+    @Nonnull private List<String> tagAssignments = new ArrayList<>();
 
     /** Omit namespaces from root element? */
     @Parameter(names = {"--omit-namespaces"}, description="Omit namespace declarations (assumes surrounding document context)")
@@ -215,6 +234,14 @@ public class MetadataGenCommandLineArguments extends AbstractIdPHomeAwareCommand
     @Parameter(names = {"--batch-file", "--batch"}, description="Path to a batch of commands to run")
     @Nullable private String batchFile;
     
+    /** Endpoint layout template. */
+    @Parameter(names = {"--layout-file", "--layout"}, description="Path to an EntityDescriptor endpoint layout template")
+    @Nullable private String layoutFile;
+    
+    /** Hostname(s) to plug into layout template. */
+    @Parameter(names = {"--hostname", "-h"}, description="Hostname(s) to plug into layout template")
+    @Nonnull private List<String> hostnames = new ArrayList<>();
+    
     /** {@inheritDoc} */
     @Nullable public String getEntityID() {
         return entityID;
@@ -233,28 +260,27 @@ public class MetadataGenCommandLineArguments extends AbstractIdPHomeAwareCommand
         return getLanguageRanges().get(0).getRange();
     }
 
-   /** {@inheritDoc} */
-   @Nullable public String getDisplayName() {
+    /** {@inheritDoc} */
+    @Nullable public String getDisplayName() {
         return displayName;
     }
 
-   /** {@inheritDoc} */
+    /** {@inheritDoc} */
     @Nullable public String getDescription() {
         return description;
     }
 
     /** {@inheritDoc} */
     @Nullable public Logo getLogo() {
+        
+        if (logoObject != null) {
+            return logoObject;
+        }
+
         if (logoURL == null) {
             return null;
         }
-        
-        synchronized(this) {
-            if (logoObject != null) {
-                return logoObject;
-            }
-        }
-        
+
         final XMLObjectBuilderFactory bf = XMLObjectProviderRegistrySupport.getBuilderFactory();
         
         final SAMLObjectBuilder<Logo> builder =
@@ -270,10 +296,7 @@ public class MetadataGenCommandLineArguments extends AbstractIdPHomeAwareCommand
             logo.setHeight(logoHeight);
         }
 
-        synchronized(this) {
-            logoObject = logo;
-        }
-        
+        logoObject = logo;
         return logo;
     }
     
@@ -289,32 +312,70 @@ public class MetadataGenCommandLineArguments extends AbstractIdPHomeAwareCommand
     
     /** {@inheritDoc} */
     @Nullable public SPSSODescriptor getSPSSODescriptor() {
+        
+        if (spRole != null) {
+            return spRole;
+        }
+        
         if (!sp) {
             return null;
         }
-        
-        synchronized(this) {
-            if (spRole != null) {
-                return spRole;
+
+        final XMLObjectBuilderFactory bf = XMLObjectProviderRegistrySupport.getBuilderFactory();
+
+        SPSSODescriptor role = null;
+        if (layoutObject instanceof SPSSODescriptor downcast) {
+            role = downcast;
+        } else if (layoutObject instanceof EntityDescriptor entity) {
+            final List<RoleDescriptor> children = entity.getRoleDescriptors(SPSSODescriptor.DEFAULT_ELEMENT_NAME);
+            if (!children.isEmpty()) {
+                role = (SPSSODescriptor) children.get(0);
+            } else {
+                throw new IllegalArgumentException("Layout object did not contain an SPSSODescriptor role.");
             }
         }
         
-        final XMLObjectBuilderFactory bf = XMLObjectProviderRegistrySupport.getBuilderFactory();
-        
-        final SAMLObjectBuilder<SPSSODescriptor> builder =
-                (SAMLObjectBuilder<SPSSODescriptor>) bf.<SPSSODescriptor>ensureBuilder(
-                        SPSSODescriptor.DEFAULT_ELEMENT_NAME);
-        final SPSSODescriptor role = builder.buildObject();
-        
-        final List<String> protocols = new ArrayList<>();
-        
-        role.getSingleLogoutServices().addAll(
-                convertEndpoints(new SingleLogoutServiceConverter(), protocols, logoutServices));
-        role.getArtifactResolutionServices().addAll(
-                convertEndpoints(new ArtifactResolutionServiceConverter(), protocols, artifactServices));
-        role.getAssertionConsumerServices().addAll(
-                convertEndpoints(new AssertionConsumerServiceConverter(), protocols, acServices));
-        
+        if (role != null) {
+            if (!hostnames.isEmpty()) {
+                // Process the various endpoints for hostname replacement.
+                try {
+                    final Collection<SingleLogoutService> slo = convertEndpoints(role.getSingleLogoutServices(),
+                            hostnames);
+                    role.getSingleLogoutServices().clear();
+                    role.getSingleLogoutServices().addAll(slo);
+                    
+                    final Collection<ArtifactResolutionService> art = convertEndpoints(role.getArtifactResolutionServices(),
+                            hostnames);
+                    role.getArtifactResolutionServices().clear();
+                    role.getArtifactResolutionServices().addAll(art);
+
+                    final Collection<AssertionConsumerService> acs = convertEndpoints(role.getAssertionConsumerServices(),
+                            hostnames);
+                    role.getAssertionConsumerServices().clear();
+                    role.getAssertionConsumerServices().addAll(acs);
+                    
+                } catch (final MarshallingException | UnmarshallingException e) {
+                    throw new IllegalArgumentException("Error cloning endpoints for hostname replacement.", e);
+                }
+            }
+        } else {
+            final SAMLObjectBuilder<SPSSODescriptor> builder =
+                    (SAMLObjectBuilder<SPSSODescriptor>) bf.<SPSSODescriptor>ensureBuilder(
+                            SPSSODescriptor.DEFAULT_ELEMENT_NAME);
+            role = builder.buildObject();
+            
+            final List<String> protocols = new ArrayList<>();
+            
+            role.getSingleLogoutServices().addAll(
+                    convertEndpoints(new SingleLogoutServiceConverter(), protocols, logoutServices));
+            role.getArtifactResolutionServices().addAll(
+                    convertEndpoints(new ArtifactResolutionServiceConverter(), protocols, artifactServices));
+            role.getAssertionConsumerServices().addAll(
+                    convertEndpoints(new AssertionConsumerServiceConverter(), protocols, acServices));
+            
+            protocols.forEach(role::addSupportedProtocol);
+        }
+
         int index = 1;
         for (final IndexedEndpoint e : role.getArtifactResolutionServices()) {
             e.setIndex(index++);
@@ -324,9 +385,7 @@ public class MetadataGenCommandLineArguments extends AbstractIdPHomeAwareCommand
         for (final IndexedEndpoint e : role.getAssertionConsumerServices()) {
             e.setIndex(index++);
         }
-        
-        protocols.forEach(role::addSupportedProtocol);
-        
+                
         if (wantAssertionsSigned) {
             role.setWantAssertionsSigned(wantAssertionsSigned);
         }
@@ -335,94 +394,145 @@ public class MetadataGenCommandLineArguments extends AbstractIdPHomeAwareCommand
             final SAMLObjectBuilder<NameIDFormat> formatBuilder =
                     (SAMLObjectBuilder<NameIDFormat>) bf.<NameIDFormat>ensureBuilder(
                             NameIDFormat.DEFAULT_ELEMENT_NAME);
-            
+
+            // Need to stash role in final variable for lambda.
+            final SPSSODescriptor roleCopy = role;
             StringSupport.normalizeStringCollection(nameIDFormats).stream()
                 .forEach(format -> {
                     final NameIDFormat obj = formatBuilder.buildObject();
                     obj.setURI(format);
-                    role.getNameIDFormats().add(obj);
+                    roleCopy.getNameIDFormats().add(obj);
                 });
         }
 
-        synchronized(this) {
-            spRole = role;
-        }
-        
+        spRole = role;
         return role;
     }
 
     /** {@inheritDoc} */
     @Nullable public IDPSSODescriptor getIDPSSODescriptor() {
+        
+        if (idpRole != null) {
+            return idpRole;
+        }
+        
         if (!idp) {
             return null;
         }
         
-        synchronized(this) {
-            if (idpRole != null) {
-                return idpRole;
+        IDPSSODescriptor role = null;
+        
+        if (layoutObject instanceof IDPSSODescriptor downcast) {
+            role = downcast;
+        } else if (layoutObject instanceof EntityDescriptor entity) {
+            final List<RoleDescriptor> children = entity.getRoleDescriptors(IDPSSODescriptor.DEFAULT_ELEMENT_NAME);
+            if (!children.isEmpty()) {
+                role = (IDPSSODescriptor) children.get(0);
             }
         }
         
-        final XMLObjectBuilderFactory bf = XMLObjectProviderRegistrySupport.getBuilderFactory();
-
-        final SAMLObjectBuilder<IDPSSODescriptor> builder =
-                (SAMLObjectBuilder<IDPSSODescriptor>) bf.<IDPSSODescriptor>ensureBuilder(
-                        IDPSSODescriptor.DEFAULT_ELEMENT_NAME);
-        final IDPSSODescriptor role = builder.buildObject();
-
-        final List<String> protocols = new ArrayList<>();
-
-        role.getSingleLogoutServices().addAll(
-                convertEndpoints(new SingleLogoutServiceConverter(), protocols, logoutServices));
-        role.getSingleSignOnServices().addAll(
-                convertEndpoints(new SingleSignOnServiceConverter(), protocols, ssoServices));
-        role.getArtifactResolutionServices().addAll(
-                convertEndpoints(new ArtifactResolutionServiceConverter(), protocols, artifactServices));
+        if (role != null) {
+            if (!hostnames.isEmpty()) {
+                // Process the various endpoints for hostname replacement.
+                try {
+                    final Collection<SingleLogoutService> slo = convertEndpoints(role.getSingleLogoutServices(),
+                            CollectionSupport.singletonList(hostnames.get(0)));
+                    role.getSingleLogoutServices().clear();
+                    role.getSingleLogoutServices().addAll(slo);
+                    
+                    final Collection<SingleSignOnService> sso = convertEndpoints(role.getSingleSignOnServices(),
+                            CollectionSupport.singletonList(hostnames.get(0)));
+                    role.getSingleSignOnServices().clear();
+                    role.getSingleSignOnServices().addAll(sso);
+                    
+                    final Collection<ArtifactResolutionService> art = convertEndpoints(role.getArtifactResolutionServices(),
+                            CollectionSupport.singletonList(hostnames.get(0)));
+                    role.getArtifactResolutionServices().clear();
+                    role.getArtifactResolutionServices().addAll(art);
+                } catch (final MarshallingException | UnmarshallingException e) {
+                    throw new IllegalArgumentException("Error cloning endpoints for hostname replacement.", e);
+                }
+            }
+        } else {
+            final XMLObjectBuilderFactory bf = XMLObjectProviderRegistrySupport.getBuilderFactory();
+            final SAMLObjectBuilder<IDPSSODescriptor> builder =
+                    (SAMLObjectBuilder<IDPSSODescriptor>) bf.<IDPSSODescriptor>ensureBuilder(
+                            IDPSSODescriptor.DEFAULT_ELEMENT_NAME);
+            role = builder.buildObject();
+    
+            final List<String> protocols = new ArrayList<>();
+    
+            role.getSingleLogoutServices().addAll(
+                    convertEndpoints(new SingleLogoutServiceConverter(), protocols, logoutServices));
+            role.getSingleSignOnServices().addAll(
+                    convertEndpoints(new SingleSignOnServiceConverter(), protocols, ssoServices));
+            role.getArtifactResolutionServices().addAll(
+                    convertEndpoints(new ArtifactResolutionServiceConverter(), protocols, artifactServices));
+    
+            protocols.forEach(role::addSupportedProtocol);
+        }
 
         int index = 1;
         for (final IndexedEndpoint e : role.getArtifactResolutionServices()) {
             e.setIndex(index++);
         }
-        
-        protocols.forEach(role::addSupportedProtocol);
 
-        synchronized(this) {
-            idpRole = role;
-        }
-        
+        idpRole = role;
         return role;
     }
 
     /** {@inheritDoc} */
     @Nullable public AttributeAuthorityDescriptor getAttributeAuthorityDescriptor() {
+        
+        if (aaRole != null) {
+            return aaRole;
+        }
+
         if (!aa) {
             return null;
         }
+
+        AttributeAuthorityDescriptor role = null;
         
-        synchronized(this) {
-            if (aaRole != null) {
-                return aaRole;
+        if (layoutObject instanceof AttributeAuthorityDescriptor downcast) {
+            role = downcast;
+        } else if (layoutObject instanceof EntityDescriptor entity) {
+            final List<RoleDescriptor> children = entity.getRoleDescriptors(AttributeAuthorityDescriptor.DEFAULT_ELEMENT_NAME);
+            if (!children.isEmpty()) {
+                role = (AttributeAuthorityDescriptor) children.get(0);
+            } else {
+                throw new IllegalArgumentException("Layout object did not contain an AttributeAuthorityDescriptor role.");
             }
         }
-
-        final XMLObjectBuilderFactory bf = XMLObjectProviderRegistrySupport.getBuilderFactory();
-        
-        final SAMLObjectBuilder<AttributeAuthorityDescriptor> builder =
-                (SAMLObjectBuilder<AttributeAuthorityDescriptor>) bf.<AttributeAuthorityDescriptor>ensureBuilder(
-                        AttributeAuthorityDescriptor.DEFAULT_ELEMENT_NAME);
-        final AttributeAuthorityDescriptor role = builder.buildObject();
-        
-        final List<String> protocols = new ArrayList<>();
-
-        role.getAttributeServices().addAll(
-                convertEndpoints(new AttributeServiceConverter(), protocols, attributeServices));
         
-        protocols.forEach(role::addSupportedProtocol);
-        
-        synchronized(this) {
-            aaRole = role;
+        if (role != null) {
+            if (!hostnames.isEmpty()) {
+                // Process the various endpoints for hostname replacement.
+                try {
+                    final Collection<AttributeService> attr = convertEndpoints(role.getAttributeServices(),
+                            CollectionSupport.singletonList(hostnames.get(0)));
+                    role.getAttributeServices().clear();
+                    role.getAttributeServices().addAll(attr);
+                } catch (final MarshallingException | UnmarshallingException e) {
+                    throw new IllegalArgumentException("Error cloning endpoints for hostname replacement.", e);
+                }
+            }
+        } else {
+            final XMLObjectBuilderFactory bf = XMLObjectProviderRegistrySupport.getBuilderFactory();
+            final SAMLObjectBuilder<AttributeAuthorityDescriptor> builder =
+                    (SAMLObjectBuilder<AttributeAuthorityDescriptor>) bf.<AttributeAuthorityDescriptor>ensureBuilder(
+                            AttributeAuthorityDescriptor.DEFAULT_ELEMENT_NAME);
+            role = builder.buildObject();
+            
+            final List<String> protocols = new ArrayList<>();
+    
+            role.getAttributeServices().addAll(
+                    convertEndpoints(new AttributeServiceConverter(), protocols, attributeServices));
+            
+            protocols.forEach(role::addSupportedProtocol);
         }
         
+        aaRole = role;
         return role;
     }
     
@@ -449,16 +559,15 @@ public class MetadataGenCommandLineArguments extends AbstractIdPHomeAwareCommand
     
     /** {@inheritDoc} */
     @Nonnull @Unmodifiable @NotLive public Map<String,Collection<String>> getTagAssignments() {
+        
+        if (tagMap != null) {
+            return tagMap.asMap();
+        }
+
         if (tagAssignments.isEmpty()) {
             return CollectionSupport.emptyMap();
         }
-        
-        synchronized(this) {
-            if (tagMap != null) {
-                return tagMap.asMap();
-            }
-        }
-        
+
         final Multimap<String,String> map = ArrayListMultimap.create();
         
         for (final String assignment : tagAssignments) {
@@ -476,12 +585,8 @@ public class MetadataGenCommandLineArguments extends AbstractIdPHomeAwareCommand
             }
         }
         
-        synchronized(this) {
-            tagMap = map;
-        }
-        
-        assert tagMap != null;
-        return tagMap.asMap();
+        tagMap = map;
+        return map.asMap();
     }
     
     /** {@inheritDoc} */
@@ -573,14 +678,17 @@ public class MetadataGenCommandLineArguments extends AbstractIdPHomeAwareCommand
         if (!sp && !idp && !aa) {
             sp = true;
         }
+        
+        if (!hostnames.isEmpty() && layoutFile == null) {
+            throw new IllegalArgumentException("Hostname option requires a layout template.");
+        }
     }
-
+    
     /** {@inheritDoc} */
     @Override
     public void printHelp(@Nonnull final PrintStream out) {
         super.printHelp(out);
         
-        
         out.println(String.format("  %-20s %s", "--entityID, --client_id, --id", "The entityID (or client_id, etc.)"));
         out.println(String.format("  %-20s %s", "--scope", "Scope extension value"));
         
@@ -619,10 +727,37 @@ public class MetadataGenCommandLineArguments extends AbstractIdPHomeAwareCommand
         out.println(String.format("  %-20s %s", "--omit-namespaces", "Omit namespaces on root element."));
         out.println(String.format("  %-20s %s", "--output, --out", "Output file path."));
         
+        out.println(String.format("  %-20s %s", "--layout-file, --layout", "Endpoint layout file path"));
+        out.println(String.format("  %-20s %s", "--hostname, -h", "Hostname(s) to plug into layout template"));
+        
         out.println(String.format("  %-20s %s", "--batch-file, --batch", "Batch file path."));
         out.println();
     }
     
+    /**
+     * Load the layout object if necessary.
+     * 
+     * @throws IOException if unable to load from file 
+     * @throws UnmarshallingException if unable to parse the file
+     * @throws XMLParserException if unable to parse the file
+     */
+    public void loadLayoutObject() throws IOException, XMLParserException, UnmarshallingException {
+        if (layoutObject != null || layoutFile == null) {
+            return;
+        }
+        
+        final ParserPool parser = XMLObjectProviderRegistrySupport.getParserPool();
+        if (parser == null) {
+            throw new IllegalArgumentException("ParserPool unavailable, cannot accomodate layout file parameter.");
+        }
+        try (final FileInputStream in = new FileInputStream(layoutFile)) {
+            layoutObject = XMLObjectSupport.unmarshallFromInputStream(parser, in);
+            if (!(layoutObject instanceof EntityDescriptor || layoutObject instanceof RoleDescriptor)) {
+                throw new IllegalArgumentException("Unmarshalled layout object was not of a supported type");
+            }
+        }
+    }
+    
     /**
      * Load a certificate from a file in a standard format and produce a base64-encoded DER string.
      * 
@@ -641,7 +776,42 @@ public class MetadataGenCommandLineArguments extends AbstractIdPHomeAwareCommand
     }
     
     /**
-     * Convert the expressions into endpoints.
+     * If the input endpoints contain a location requiring hostname replacement, they're cloned,
+     * the location adjusted for each possible hostname, and the clones are returned.
+     * 
+     * @param <T> type of endpoint
+     * @param inputs endpoint template(s)
+     * @param hostnames hostname(s) to replace with
+     * 
+     * @return the final collection of endpoints to include
+     * 
+     * @throws UnmarshallingException if cloning fails 
+     * @throws MarshallingException if cloning fails
+     */
+    @Nonnull @Unmodifiable @NotLive private <T extends Endpoint> Collection<T> convertEndpoints(
+            @Nonnull final Collection<T> inputs, @Nonnull final Collection<String> hostnames)
+                    throws MarshallingException, UnmarshallingException {
+        
+        final Collection<T> accumulator = new ArrayList<>();
+        
+        for (final T originalEndpoint : inputs) {
+            final String originalLocation = originalEndpoint.getLocation();
+            if (originalLocation != null && originalLocation.contains("${hostname}")) {
+                for (final String hostname : hostnames) {
+                    final T cloned = XMLObjectSupport.cloneXMLObject(originalEndpoint);
+                    cloned.setLocation(originalLocation.replace("${hostname}", hostname));
+                    accumulator.add(cloned);
+                }
+            } else {
+                accumulator.add(originalEndpoint);
+            }
+        }
+        
+        return accumulator;
+    }
+    
+    /**
+     * Convert binding/location expressions into endpoints.
      * 
      * @param <T> endpoint type
      * @param converter endpoint converter
@@ -657,7 +827,7 @@ public class MetadataGenCommandLineArguments extends AbstractIdPHomeAwareCommand
                 .map(s -> converter.apply(s, protocols))
                 .collect(CollectionSupport.nonnullCollector(Collectors.toUnmodifiableList())).get();
     }
-    
+
     /**
      * Parse string into a {@link ContactPerson} object.
      * 
diff --git a/metadatagen-impl/src/main/resources/net/shibboleth/idp/plugin/metadatagen/conf/examples/shibmd-layout.xml b/metadatagen-impl/src/main/resources/net/shibboleth/idp/plugin/metadatagen/conf/examples/shibmd-layout.xml
new file mode 100644
index 0000000..f44f0cb
--- /dev/null
+++ b/metadatagen-impl/src/main/resources/net/shibboleth/idp/plugin/metadatagen/conf/examples/shibmd-layout.xml
@@ -0,0 +1,51 @@
+<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata">
+
+    <!--
+    These include a full range of endpoints that likely exceeds what you will actually want to include.
+    In particular: The POST-SimpleSign and Artifact bindings are generally unused, and the SAML 1.1 endpoints
+        should usually be omitted along with the non-SAML 2.0 protocol values.
+    -->
+
+    <IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol urn:oasis:names:tc:SAML:1.1:protocol urn:mace:shibboleth:1.0">
+        <ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://${hostname}/idp/profile/SAML2/SOAP/ArtifactResolution" />
+        <ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:1.0:bindings:SOAP-binding" Location="https://${hostname}/idp/profile/SAML1/SOAP/ArtifactResolution" />
+
+        <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://${hostname}/idp/profile/SAML2/Redirect/SLO"/>
+        <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://${hostname}/idp/profile/SAML2/POST/SLO"/>
+        <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign" Location="https://${hostname}/idp/profile/SAML2/POST-SimpleSign/SLO"/>
+        <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://${hostname}/idp/profile/SAML2/Artifact/SLO"/>
+        <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://${hostname}/idp/profile/SAML2/SOAP/SLO"/>
+
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://${hostname}/idp/profile/SAML2/Redirect/SSO"/>
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://${hostname}/idp/profile/SAML2/POST/SSO"/>
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign" Location="https://${hostname}/idp/profile/SAML2/POST-SimpleSign/SSO"/>
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://${hostname}/idp/profile/SAML2/Artifact/SSO"/>
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://${hostname}/idp/profile/SAML2/SOAP/ECP"/>
+        <SingleSignOnService Binding="urn:mace:shibboleth:1.0:profiles:AuthnRequest" Location="https://${hostname}/idp/profile/Shibboleth/SSO"/>
+    </IDPSSODescriptor>
+
+    <AttributeAuthorityDescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol urn:oasis:names:tc:SAML:1.1:protocol">
+        <AttributeService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://${hostname}/idp/profile/SAML2/SOAP/AttributeQuery"/>
+        <AttributeService Binding="urn:oasis:names:tc:SAML:1.0:bindings:SOAP-binding" Location="https://${hostname}/idp/profile/SAML1/SOAP/AttributeQuery"/>
+    </AttributeAuthorityDescriptor>
+
+    <!-- These conform to a Shibboleth SP, *not* the IdP's proxying endpoint. -->
+    
+    <SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol urn:oasis:names:tc:SAML:1.1:protocol">
+        <ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://${hostname}/Shibboleth.sso/Artifact/SOAP" />
+    
+        <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://${hostname}/Shibboleth.sso/SLO/SOAP"/>
+        <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://${hostname}/Shibboleth.sso/SLO/Redirect"/>
+        <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://${hostname}/Shibboleth.sso/SLO/POST"/>
+        <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign" Location="https://${hostname}/Shibboleth.sso/SLO/POST-SimpleSign" />
+        <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://${hostname}/Shibboleth.sso/SLO/Artifact"/>
+    
+        <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://${hostname}/Shibboleth.sso/SAML2/POST" />
+        <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign" Location="https://${hostname}/Shibboleth.sso/SAML2/POST-SimpleSign" />
+        <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://${hostname}/Shibboleth.sso/SAML2/Artifact" />
+        <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:PAOS" Location="https://${hostname}/Shibboleth.sso/SAML2/ECP" />
+        <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:1.0:profiles:browser-post" Location="https://${hostname}/Shibboleth.sso/SAML/POST" />
+        <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:1.0:profiles:artifact-01" Location="https://${hostname}/Shibboleth.sso/SAML/Artifact" />
+    </SPSSODescriptor>
+    
+</EntityDescriptor>
\ No newline at end of file
diff --git a/metadatagen-impl/src/main/resources/net/shibboleth/idp/plugin/metadatagen/conf/velocity.xml b/metadatagen-impl/src/main/resources/net/shibboleth/idp/plugin/metadatagen/conf/velocity.xml
index b4471ec..79af972 100644
--- a/metadatagen-impl/src/main/resources/net/shibboleth/idp/plugin/metadatagen/conf/velocity.xml
+++ b/metadatagen-impl/src/main/resources/net/shibboleth/idp/plugin/metadatagen/conf/velocity.xml
@@ -12,32 +12,34 @@
        default-init-method="initialize"
        default-destroy-method="destroy">
 
-    <!-- Needed to allow for auto-wiring of descriptors and other components. -->
-    <context:annotation-config />
-
-    <bean id="shibboleth.PropertySourcesPlaceholderConfigurer"
-        class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer"
-        p:placeholderPrefix="%{" p:placeholderSuffix="}" />
-
-    <!-- This bean MUST be called "conversionService" to work properly. -->
-    <bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
-        <property name="converters">
-            <set>
-                <bean class="net.shibboleth.shared.spring.config.StringToIPRangeConverter" />
-                <bean class="net.shibboleth.shared.spring.config.BooleanToPredicateConverter" />
-                <bean class="net.shibboleth.shared.spring.config.StringBooleanToPredicateConverter" />
-                <bean class="net.shibboleth.shared.spring.config.StringToResourceConverter" />
-                <bean class="net.shibboleth.shared.spring.config.StringToDurationConverter" />
-                <bean class="net.shibboleth.shared.spring.config.StringToPeriodConverter" />
-            </set>
+    <bean id="shibboleth.OpenSAMLConfig" class="net.shibboleth.profile.spring.impl.OpenSAMLConfigBean"
+        p:parserPool-ref="shibboleth.DefaultParserPool" />
+
+    <bean id="shibboleth.DefaultParserPool" class="net.shibboleth.shared.xml.impl.BasicParserPool"
+            p:maxPoolSize="100"
+            p:coalescing="true"
+            p:ignoreComments="true"
+            p:ignoreElementContentWhitespace="true"
+            p:namespaceAware="true"
+            p:entityResolver-ref="shibboleth.ThrowingEntityResolver"
+            p:securityManagerAttributeName="http://apache.org/xml/properties/security-manager">
+        <property name="builderFeatures">
+            <map>
+                <entry key="http://apache.org/xml/features/disallow-doctype-decl">
+                    <util:constant static-field="java.lang.Boolean.TRUE" />
+                </entry>
+                <entry key="http://apache.org/xml/features/validation/schema/normalized-value">
+                    <util:constant static-field="java.lang.Boolean.FALSE" />
+                </entry>
+                <entry key="http://javax.xml.XMLConstants/feature/secure-processing">
+                    <util:constant static-field="java.lang.Boolean.TRUE" />
+                </entry>
+            </map>
         </property>
     </bean>
-
-    <!-- This BeanPostProcessor auto-sets identifiable beans with the bean name (if not already set). -->
-    <bean id="shibboleth.IdentifiableBeanPostProcessor"
-        class="net.shibboleth.shared.spring.config.IdentifiableBeanPostProcessor" />
-
-    <bean id="shibboleth.OpenSAMLConfig" class="net.shibboleth.profile.spring.impl.OpenSAMLConfigBean" />
+    
+    <bean id="shibboleth.ThrowingEntityResolver"
+        class="net.shibboleth.shared.xml.impl.ThrowingEntityResolver" />
 
     <util:map id="shibboleth.DefaultVelocityEngineProperties">
         <entry key="parser.space_gobbling" value="%{idp.velocity.space.gobbling:bc}" />
@@ -51,6 +53,6 @@
 
     <bean id="shibboleth.VelocityEngine" class="net.shibboleth.shared.spring.velocity.VelocityEngineFactoryBean"
         depends-on="shibboleth.OpenSAMLConfig"
-        p:velocityPropertiesMap="#{getObject('shibboleth.VelocityEngineProperties') ?: getObject('shibboleth.DefaultVelocityEngineProperties')}" />
+        p:velocityPropertiesMap-ref="shibboleth.DefaultVelocityEngineProperties" />
 
  </beans>
diff --git a/metadatagen-impl/src/main/resources/net/shibboleth/idp/plugin/metadatagen/impl/module.properties b/metadatagen-impl/src/main/resources/net/shibboleth/idp/plugin/metadatagen/impl/module.properties
index e8589a8..8c85e1e 100644
--- a/metadatagen-impl/src/main/resources/net/shibboleth/idp/plugin/metadatagen/impl/module.properties
+++ b/metadatagen-impl/src/main/resources/net/shibboleth/idp/plugin/metadatagen/impl/module.properties
@@ -1,18 +1,23 @@
 # Example Properties defining an authentication module.
 
 # Class to Module ID mappings
-net.shibboleth.idp.plugin.metadatagen.impl.MetadataGenModule = idp.metadatagen
+net.shibboleth.idp.plugin.metadatagen.impl.MetadataGenModule = idp.plugin.MetadataGen
 
 # Module Owner
-idp.metadatagen.plugin = net.shibboleth.idp.plugin.metadatagen
+idp.plugin.MetadataGen.plugin = net.shibboleth.idp.plugin.metadatagen
 
-idp.metadatagen.name = Metadata Generator (Beta)
-idp.metadatagen.desc = A test Metadata Generator
-idp.metadatagen.url = /Metadatagen
-idp.metadatagen.1.src = /net/shibboleth/idp/plugin/metadatagen/bin/metadatagen.sh
-idp.metadatagen.1.dest = bin/metadatagen.sh
-idp.metadatagen.1.replace = true
-idp.metadatagen.1.exec = true
-idp.metadatagen.2.src = /net/shibboleth/idp/plugin/metadatagen/bin/metadatagen.bat
-idp.metadatagen.2.dest = bin/metadatagen.bat
-idp.metadatagen.2.replace = true
+idp.plugin.MetadataGen.name = Metadata Generator (Beta)
+idp.plugin.MetadataGen.desc = A test Metadata Generator
+idp.plugin.MetadataGen.url = /Metadatagen
+idp.plugin.MetadataGen.1.src = /net/shibboleth/idp/plugin/metadatagen/bin/metadatagen.sh
+idp.plugin.MetadataGen.1.dest = bin/metadatagen.sh
+idp.plugin.MetadataGen.1.replace = true
+idp.plugin.MetadataGen.1.exec = true
+idp.plugin.MetadataGen.1.windows = false
+idp.plugin.MetadataGen.2.src = /net/shibboleth/idp/plugin/metadatagen/bin/metadatagen.bat
+idp.plugin.MetadataGen.2.dest = bin/metadatagen.bat
+idp.plugin.MetadataGen.2.replace = true
+idp.plugin.MetadataGen.2.nonwindows = false
+idp.plugin.MetadataGen.3.src = /net/shibboleth/idp/plugin/metadatagen/conf/examples/shibmd-layout.xml
+idp.plugin.MetadataGen.3.dest = conf/examples/shibmd-layout.xml
+idp.plugin.MetadataGen.3.replace = true
diff --git a/metadatagen-impl/src/test/java/net/shibboleth/idp/plugin/metadatagen/impl/MetadataGenTest.java b/metadatagen-impl/src/test/java/net/shibboleth/idp/plugin/metadatagen/impl/MetadataGenTest.java
index 11d2965..6938de7 100644
--- a/metadatagen-impl/src/test/java/net/shibboleth/idp/plugin/metadatagen/impl/MetadataGenTest.java
+++ b/metadatagen-impl/src/test/java/net/shibboleth/idp/plugin/metadatagen/impl/MetadataGenTest.java
@@ -33,6 +33,7 @@ public class MetadataGenTest {
     //      src/test/resources/net/shibboleth/idp/plugin/metadatagen/impl/extra2
     private final static String IDP_HOME =  "/Users/scantor/Documents/shibboleth5/java-identity-provider/idp-conf-impl/src/main/resources/net/shibboleth/idp/module";
     private final static String BATCH_PATH =  "src/test/resources/net/shibboleth/idp/plugin/metadatagen/impl/batch.txt";
+    private final static String LAYOUT_PATH =  "src/main/resources/net/shibboleth/idp/plugin/metadatagen/conf/examples/shibmd-layout.xml";
     private final boolean enabled = false;
 
     @Test(enabled = enabled) public void testSimple() {
@@ -80,4 +81,21 @@ public class MetadataGenTest {
             AbstractCommandLine.RC_OK);
     }
 
+    @Test(enabled = enabled) public void testLayoutSP() {
+        assertEquals(MetadataGenCLI.runMain(
+                new String[] {
+                        "--home", IDP_HOME,
+                        "--layout", LAYOUT_PATH,
+                        "--entityID", "https://sp.example.org",
+                        "--sp",
+                        "--idp",
+                        "--aa",
+                        "--cert", "/Users/scantor/Desktop/webauth2.crt",
+                        "-h", "sp.example.org",
+                        "-h", "sp2.example.org",
+                        "--tag", "urn:oasis:names:tc:SAML:profiles:subject-id:req=subject-id",
+                        }),
+            AbstractCommandLine.RC_OK);
+    }
+    
 }
\ No newline at end of file
diff --git a/metadatagen-impl/src/test/resources/net/shibboleth/idp/plugin/metadatagen/impl/batch.txt b/metadatagen-impl/src/test/resources/net/shibboleth/idp/plugin/metadatagen/impl/batch.txt
index 8737aee..2b9a0a7 100644
--- a/metadatagen-impl/src/test/resources/net/shibboleth/idp/plugin/metadatagen/impl/batch.txt
+++ b/metadatagen-impl/src/test/resources/net/shibboleth/idp/plugin/metadatagen/impl/batch.txt
@@ -11,7 +11,7 @@
 --aa
 --entityID https://sp.example.org
 --scope example.org
---nameid-format NameIDType.EMAIL
+--nameid-format urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
 --cert /Users/scantor/Desktop/webauth2.crt
 --sso Redirect1/idp.example.org/idp/profile/SAML/SSO/Redirect
 --sso Redirect/idp.example.org/idp/profile/SAML2/SSO/Redirect

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


More information about the commits mailing list