[java-opensaml] branch main updated: IDP-1870: Detect duplicate entityIDs when ingesting metadata

Brent Putman putmanb at georgetown.edu
Fri Nov 19 04:29:39 UTC 2021


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

putmanb pushed a commit to branch main
in repository java-opensaml.

View the commit online:
http://git.shibboleth.net/view/?p=java-opensaml.git;a=commit;h=4f8b4c77b747cb5c70eb904d0a9bcb6b2b3082f0

The following commit(s) were added to refs/heads/main by this push:
     new 4f8b4c77b IDP-1870: Detect duplicate entityIDs when ingesting metadata
4f8b4c77b is described below

commit 4f8b4c77b747cb5c70eb904d0a9bcb6b2b3082f0
Author: Brent Putman <putmanb at georgetown.edu>
AuthorDate: Thu Nov 18 21:36:48 2021 -0500

    IDP-1870: Detect duplicate entityIDs when ingesting metadata
---
 .../entity/DetectDuplicateEntityIDsCriterion.java  |  80 +++++++++++++
 .../resolver/ChainingMetadataResolver.java         | 130 ++++++++++++++++++++-
 .../resolver/DetectDuplicateEntityIDs.java         |  37 ++++++
 3 files changed, 246 insertions(+), 1 deletion(-)

diff --git a/opensaml-saml-api/src/main/java/org/opensaml/saml/metadata/criteria/entity/DetectDuplicateEntityIDsCriterion.java b/opensaml-saml-api/src/main/java/org/opensaml/saml/metadata/criteria/entity/DetectDuplicateEntityIDsCriterion.java
new file mode 100644
index 000000000..0d8d4fbb1
--- /dev/null
+++ b/opensaml-saml-api/src/main/java/org/opensaml/saml/metadata/criteria/entity/DetectDuplicateEntityIDsCriterion.java
@@ -0,0 +1,80 @@
+/*
+ * 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 org.opensaml.saml.metadata.criteria.entity;
+
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.saml.metadata.resolver.DetectDuplicateEntityIDs;
+
+import com.google.common.base.MoreObjects;
+
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.resolver.Criterion;
+
+/**
+ * Criterion which holds an instance of {@link DetectDuplicateEntityIDs}.
+ */
+public class DetectDuplicateEntityIDsCriterion implements Criterion {
+    
+    /** The configured duplicate detection value. **/
+    @Nonnull private DetectDuplicateEntityIDs value;
+
+    /**
+     * Constructor.
+     *
+     * @param detect the value used for duplicate detection
+     */
+    public DetectDuplicateEntityIDsCriterion(@Nonnull final DetectDuplicateEntityIDs detect) {
+        value = Constraint.isNotNull(detect, "DetectDuplicateEntityIDs was null");
+    }
+    
+    /**
+     * Get the configured value.
+     * 
+     * @return the criterion value
+     */
+    @Nonnull public DetectDuplicateEntityIDs getValue() {
+        return value;
+    }
+
+    /** {@inheritDoc} */
+    public int hashCode() {
+        return value.hashCode();
+    }
+
+    /** {@inheritDoc} */
+    public boolean equals(final Object other) {
+        if (this == other) {
+            return true;
+        }
+        
+        if (other instanceof DetectDuplicateEntityIDsCriterion) {
+            return Objects.equals(this.value, ((DetectDuplicateEntityIDsCriterion)other).value);
+        }
+        
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    public String toString() {
+        return MoreObjects.toStringHelper(this).add("value", value).toString();
+    }
+
+}
diff --git a/opensaml-saml-api/src/main/java/org/opensaml/saml/metadata/resolver/ChainingMetadataResolver.java b/opensaml-saml-api/src/main/java/org/opensaml/saml/metadata/resolver/ChainingMetadataResolver.java
index 89e970449..e6048cc8b 100644
--- a/opensaml-saml-api/src/main/java/org/opensaml/saml/metadata/resolver/ChainingMetadataResolver.java
+++ b/opensaml-saml-api/src/main/java/org/opensaml/saml/metadata/resolver/ChainingMetadataResolver.java
@@ -21,6 +21,10 @@ import java.time.Instant;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -34,6 +38,7 @@ import net.shibboleth.utilities.java.support.component.ComponentSupport;
 import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
 import net.shibboleth.utilities.java.support.resolver.ResolverException;
 
+import org.opensaml.saml.metadata.criteria.entity.DetectDuplicateEntityIDsCriterion;
 import org.opensaml.saml.metadata.resolver.filter.MetadataFilter;
 import org.opensaml.saml.saml2.metadata.EntityDescriptor;
 import org.slf4j.Logger;
@@ -53,10 +58,14 @@ public class ChainingMetadataResolver extends AbstractIdentifiableInitializableC
 
     /** Registered resolvers. */
     @Nonnull @NonnullElements private List<MetadataResolver> resolvers;
+    
+    /** Strategy for detecting duplicate entityIDs across resolvers. */
+    @Nonnull private DetectDuplicateEntityIDs detectDuplicateEntityIDs;
 
     /** Constructor. */
     public ChainingMetadataResolver() {
         resolvers = Collections.emptyList();
+        detectDuplicateEntityIDs = DetectDuplicateEntityIDs.Off;
     }
 
     /**
@@ -88,6 +97,24 @@ public class ChainingMetadataResolver extends AbstractIdentifiableInitializableC
         resolvers = List.copyOf(newResolvers);
     }
 
+    /**
+     * Get the strategy for detecting duplicate entityIDs across resolvers.
+     * 
+     * @return the configured strategy
+     */
+    @Nonnull public DetectDuplicateEntityIDs getDetectDuplicateEntityIDs() {
+        return detectDuplicateEntityIDs;
+    }
+
+    /**
+     * Set the strategy for detecting duplicate entityIDs across resolvers.
+     * 
+     * @param strategy the strategy to configure
+     */
+    public void setDetectDuplicateEntityIDs(@Nullable final DetectDuplicateEntityIDs strategy) {
+        detectDuplicateEntityIDs = strategy != null ? strategy : DetectDuplicateEntityIDs.Off;
+    }
+
     /** {@inheritDoc} */
     @Override public boolean isRequireValidMetadata() {
         log.warn("Attempt to access unsupported requireValidMetadata property on ChainingMetadataResolver");
@@ -129,12 +156,34 @@ public class ChainingMetadataResolver extends AbstractIdentifiableInitializableC
     @Override
     @Nonnull public Iterable<EntityDescriptor> resolve(@Nullable final CriteriaSet criteria) throws ResolverException {
         ComponentSupport.ifNotInitializedThrowUninitializedComponentException(this);
+        
+        DetectDuplicateEntityIDs detectDuplicates = getDetectDuplicateEntityIDs();
+        if (criteria.contains(DetectDuplicateEntityIDsCriterion.class)) {
+            detectDuplicates = criteria.get(DetectDuplicateEntityIDsCriterion.class).getValue();
+        }
+        log.trace("Effecgive DetectDuplicateEntityIDs value is: {}", detectDuplicates);
 
+        Iterable<EntityDescriptor> result = null;
+        Set<String> resultEntityIDs = null;
         for (final MetadataResolver resolver : resolvers) {
             try {
+                if (result != null) {
+                    detectDuplicateEntityIDs(resolver, criteria, resultEntityIDs, detectDuplicates);
+                    continue;
+                }
+                
                 final Iterable<EntityDescriptor> descriptors = resolver.resolve(criteria);
                 if (descriptors != null && descriptors.iterator().hasNext()) {
-                    return descriptors;
+                    if (detectDuplicates == DetectDuplicateEntityIDs.Off) {
+                        log.trace("Resolved EntityDescriptor(s) from '{}', duplicate detection disabled, returning",
+                                resolver.getId());
+                        return descriptors;
+                    }
+                    
+                    log.trace("Resolved EntityDescriptor(s) from '{}', duplicate detection enabled, continuing",
+                            resolver.getId());
+                    result = descriptors;
+                    resultEntityIDs = collectEntityIDs(result);
                 }
             } catch (final ResolverException e) {
                 log.warn("Error retrieving metadata from resolver of type {}, proceeding to next resolver",
@@ -143,9 +192,88 @@ public class ChainingMetadataResolver extends AbstractIdentifiableInitializableC
             }
         }
 
+        if (result != null) {
+            return result;
+        }
         return Collections.emptyList();
     }
     
+    /**
+     * Perform duplicate entityID detection.
+     * 
+     * @param resolver the metadata resolver over which to perform duplicate detection
+     * @param criteria the current criteria set
+     * @param resultEntityIDs the set of entityIDs contained in the effective results to be returned
+     * @param detectDuplicates the effective strategy for duplicate detection
+     */
+    // Checkstyle: CyclomaticComplexity OFF
+    private void detectDuplicateEntityIDs(final @Nonnull MetadataResolver resolver,
+            final @Nonnull CriteriaSet criteria,
+            final @Nullable Set<String> resultEntityIDs,
+            final @Nonnull DetectDuplicateEntityIDs detectDuplicates) {
+        
+        if (resultEntityIDs == null || resultEntityIDs.isEmpty()) {
+            return;
+        }
+        
+        switch(detectDuplicates) {
+            case Off:
+                return;
+            case Batch:
+                if (!BatchMetadataResolver.class.isInstance(resolver)) {
+                    return;
+                }
+                break;
+            case Dynamic:
+                if (!DynamicMetadataResolver.class.isInstance(resolver)) {
+                    return;
+                }
+                break;
+            case All:
+                break;
+            default:
+                log.warn("Saw unknown DetectDuplicateEntityIDs value, can not process: {}", detectDuplicates);
+                return; 
+        }
+        
+        log.trace("Performing duplicate entityID detection on resolver '{} of type {}",
+                resolver.getId(), resolver.getClass().getName());
+        
+        try {
+            final Iterable<EntityDescriptor> descriptors = resolver.resolve(criteria);
+            if (descriptors != null && descriptors.iterator().hasNext()) {
+                final Set<String> descriptorsEnitityIDs = collectEntityIDs(descriptors);
+
+                final Set<String> duplicates = resultEntityIDs.stream()
+                        .filter(descriptorsEnitityIDs::contains)
+                        .collect(Collectors.toSet());
+                
+                if (!duplicates.isEmpty()) {
+                    log.warn("MetadataResolver '{}' contained duplicate entityIDs relative to the returned results: {}",
+                            resolver.getId(), duplicates);
+                }
+            }
+        } catch (final ResolverException e) {
+            log.warn("During duplicate detection, error retrieving metadata from resolver '{}' of type {}",
+                    resolver.getId(), resolver.getClass().getName(), e);
+        }
+        
+    }
+    // Checkstyle: CyclomaticComplexity ON
+
+    /**
+     * Collect the unique entityIDs from the supplied iterable of entity descriptors.
+     * 
+     * @param descriptors
+     * @return the unique entityIDs from the supplied descriptors
+     */
+    private Set<String> collectEntityIDs(final @Nonnull Iterable<EntityDescriptor> descriptors) {
+        return StreamSupport.stream(descriptors.spliterator(), false)
+                .map(EntityDescriptor::getEntityID)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+    }
+
     /** {@inheritDoc} */
     public void clear() throws ResolverException {
         for (final MetadataResolver resolver : resolvers) {
diff --git a/opensaml-saml-api/src/main/java/org/opensaml/saml/metadata/resolver/DetectDuplicateEntityIDs.java b/opensaml-saml-api/src/main/java/org/opensaml/saml/metadata/resolver/DetectDuplicateEntityIDs.java
new file mode 100644
index 000000000..b95b1bc80
--- /dev/null
+++ b/opensaml-saml-api/src/main/java/org/opensaml/saml/metadata/resolver/DetectDuplicateEntityIDs.java
@@ -0,0 +1,37 @@
+/*
+ * 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 org.opensaml.saml.metadata.resolver;
+
+/**
+ * Strategy for performing detection of duplicate entityIDs across metadata resolvers.
+ */
+public enum DetectDuplicateEntityIDs {
+    
+    /** No duplicate detection. */
+    Off,
+    
+    /** Detect only with batch metadata resolvers. */
+    Batch,
+    
+    /** Detect only with dynamic metadata resolvers. */
+    Dynamic,
+    
+    /** Detect with all metadata resolvers. */
+    All;
+
+}

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


More information about the commits mailing list