[java-shib-metadata] branch main updated: JSMD-14 - Filter to add/remove Scope extension
Codeberg
noreply at shibboleth.net
Tue Jan 20 19:59:18 UTC 2026
This is an automated email from the git hooks/post-receive script.
codeberg pushed a commit to branch main
in repository java-shib-metadata.
View the commit online:
https://codeberg.org/Shibboleth/java-shib-metadata/commit/8717e1f639bfab518458f0e9e41b07138f085386
The following commit(s) were added to refs/heads/main by this push:
new 8717e1f6 JSMD-14 - Filter to add/remove Scope extension
8717e1f6 is described below
commit 8717e1f639bfab518458f0e9e41b07138f085386
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Tue Jan 20 14:59:07 2026 -0500
JSMD-14 - Filter to add/remove Scope extension
https://shibboleth.atlassian.net/browse/JSMD-14
---
.../net/shibboleth/idp/saml/xmlobject/Scope.java | 7 +-
.../idp/saml/metadata/impl/ScopeFilter.java | 229 +++++++++++++++++++++
.../idp/saml/metadata/impl/ScopeFilterTest.java | 196 ++++++++++++++++++
3 files changed, 431 insertions(+), 1 deletion(-)
diff --git a/shib-metadata-api/src/main/java/net/shibboleth/idp/saml/xmlobject/Scope.java b/shib-metadata-api/src/main/java/net/shibboleth/idp/saml/xmlobject/Scope.java
index fe9773ca..a7a35faf 100644
--- a/shib-metadata-api/src/main/java/net/shibboleth/idp/saml/xmlobject/Scope.java
+++ b/shib-metadata-api/src/main/java/net/shibboleth/idp/saml/xmlobject/Scope.java
@@ -23,7 +23,12 @@ import org.opensaml.core.xml.schema.XSString;
import net.shibboleth.shared.annotation.constraint.NotEmpty;
-/** XMLObject for the Shibboleth Scope metadata extension. */
+/**
+ * XMLObject for the Shibboleth Scope metadata extension.
+ *
+ * <p>Note that this is at least nominally now standardized by virtue of being published
+ * in our old namespace in the SAML subject-id Attribute spec.</p>
+ */
public interface Scope extends XSString {
/** Element local name. */
diff --git a/shib-metadata-impl/src/main/java/net/shibboleth/idp/saml/metadata/impl/ScopeFilter.java b/shib-metadata-impl/src/main/java/net/shibboleth/idp/saml/metadata/impl/ScopeFilter.java
new file mode 100644
index 00000000..f46f6d63
--- /dev/null
+++ b/shib-metadata-impl/src/main/java/net/shibboleth/idp/saml/metadata/impl/ScopeFilter.java
@@ -0,0 +1,229 @@
+/*
+ * Licensed 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.saml.metadata.impl;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.core.xml.XMLObject;
+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.metadata.resolver.filter.AbstractMetadataFilter;
+import org.opensaml.saml.metadata.resolver.filter.FilterException;
+import org.opensaml.saml.metadata.resolver.filter.MetadataFilterContext;
+import org.opensaml.saml.saml2.metadata.EntitiesDescriptor;
+import org.opensaml.saml.saml2.metadata.EntityDescriptor;
+import org.opensaml.saml.saml2.metadata.Extensions;
+import org.opensaml.saml.saml2.metadata.RoleDescriptor;
+import org.slf4j.Logger;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Multimap;
+
+import net.shibboleth.idp.saml.xmlobject.Scope;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * A filter that adds the {@link Scope} extension to entities in order to drive software
+ * behavior based on them.
+ *
+ * <p>The entities to annotate are identified with a {@link Predicate}, and multiple scopes can be
+ * associated with each, with and without the regexp flag.</p>
+ *
+ * <p>Note that this filter only operates on extensions defined at the {@link EntityDescriptor} level
+ * and not on individual roles. Role-specific scopes are discouraged in general.</p>
+ */
+public class ScopeFilter extends AbstractMetadataFilter {
+
+ /** Used as a value in place of an empty collection when removing only. */
+ @Nonnull @NotEmpty private static final String GUARD_VALUE = "_EMPTY";
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(ScopeFilter.class);
+
+ /** Stand-in value for an empty collection. */
+ @Nonnull private final Scope guardObject;
+
+ /** Whether to strip any existing Scopes when adding new ones. */
+ private boolean removeExistingScopes;
+
+ /** Rules for adding scopes. */
+ @Nonnull private Multimap<Predicate<EntityDescriptor>,Scope> applyMap;
+
+ /** Builder for {@link Extensions}. */
+ @Nonnull private final SAMLObjectBuilder<Extensions> extBuilder;
+
+ /** Constructor. */
+ public ScopeFilter() {
+ guardObject = XMLObjectProviderRegistrySupport.getBuilderFactory().<Scope>ensureBuilder(
+ Scope.DEFAULT_ELEMENT_NAME).buildObject(Scope.DEFAULT_ELEMENT_NAME);
+ guardObject.setValue(GUARD_VALUE);
+
+ extBuilder = (SAMLObjectBuilder<Extensions>)
+ XMLObjectProviderRegistrySupport.getBuilderFactory().<Extensions>ensureBuilder(
+ Extensions.DEFAULT_ELEMENT_NAME);
+ applyMap = ArrayListMultimap.create();
+ }
+
+ /**
+ * Set whether the filter should remove any existing scopes from an entity to which it adds new ones.
+ *
+ * <p>Defaults to false (for compatibility).</p>
+ *
+ * @param flag flag to set
+ */
+ public void setRemoveExistingScopes(final boolean flag) {
+ checkSetterPreconditions();
+ removeExistingScopes = flag;
+ }
+
+ /**
+ * Set the mappings from {@link Predicate} to Scope collection to apply.
+ *
+ * @param rules rules to apply
+ */
+ public void setRules(@Nonnull final Map<Predicate<EntityDescriptor>,Collection<Scope>> rules) {
+ checkSetterPreconditions();
+ Constraint.isNotNull(rules, "Rules map cannot be null");
+
+ applyMap = ArrayListMultimap.create(rules.size(), 1);
+ for (final Map.Entry<Predicate<EntityDescriptor>,Collection<Scope>> entry : rules.entrySet()) {
+ if (entry.getKey() != null && entry.getValue() != null) {
+ applyMap.putAll(entry.getKey(),
+ entry.getValue().isEmpty() ? CollectionSupport.singletonList(guardObject) :
+ CollectionSupport.copyToList(entry.getValue()));
+ }
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Nullable public XMLObject filter(@Nullable final XMLObject metadata, @Nonnull final MetadataFilterContext context)
+ throws FilterException {
+ checkComponentActive();
+ if (metadata == null) {
+ return null;
+ }
+
+ if (metadata instanceof EntitiesDescriptor) {
+ filterEntitiesDescriptor((EntitiesDescriptor) metadata);
+ } else {
+ filterEntityDescriptor((EntityDescriptor) metadata);
+ }
+
+ return metadata;
+ }
+
+ /**
+ * Filters entity descriptor.
+ *
+ * @param descriptor entity descriptor to filter
+ */
+ protected void filterEntityDescriptor(@Nonnull final EntityDescriptor descriptor) {
+ for (final Map.Entry<Predicate<EntityDescriptor>,Collection<Scope>> entry : applyMap.asMap().entrySet()) {
+ if (entry.getKey().test(descriptor)) {
+ // Since we only add to the roles, we need to delete anything at the entity level.
+ if (removeExistingScopes) {
+ final Extensions exts = descriptor.getExtensions();
+ if (exts != null) {
+ final Collection<XMLObject> scopes = exts.getUnknownXMLObjects(Scope.DEFAULT_ELEMENT_NAME);
+ if (!scopes.isEmpty()) {
+ log.debug("Clearing existing entity-level Scope extensions on EntityDescriptor '{}'",
+ descriptor.getEntityID());
+ scopes.clear();
+ }
+ }
+ }
+ for (final RoleDescriptor role : descriptor.getRoleDescriptors()) {
+ assert role != null;
+ filterRoleDescriptor(role, entry.getValue());
+ }
+ }
+ }
+ }
+
+ /**
+ * Filters entities descriptor.
+ *
+ * @param descriptor entities descriptor to filter
+ */
+ protected void filterEntitiesDescriptor(@Nonnull final EntitiesDescriptor descriptor) {
+
+ // First we check any contained EntitiesDescriptors.
+ for (final EntitiesDescriptor group : descriptor.getEntitiesDescriptors()) {
+ assert group != null;
+ filterEntitiesDescriptor(group);
+ }
+
+ // Next, check contained EntityDescriptors.
+ for (final EntityDescriptor entity : descriptor.getEntityDescriptors()) {
+ assert entity != null;
+ filterEntityDescriptor(entity);
+ }
+ }
+
+ /**
+ * Filters role descriptor.
+ *
+ * @param role role descriptor to filter
+ * @param scopes scopes to add
+ */
+ protected void filterRoleDescriptor(@Nonnull final RoleDescriptor role, @Nonnull final Collection<Scope> scopes) {
+ final String entityID;
+ if (role.getParent() instanceof EntityDescriptor entity) {
+ entityID = entity.getEntityID();
+ } else {
+ entityID = null;
+ }
+
+ Extensions exts = role.getExtensions();
+ if (exts != null && removeExistingScopes) {
+ log.debug("Removing existing Scope extensions from {} role in EntityDescriptor '{}'",
+ role.getElementQName(), entityID);
+ exts.getUnknownXMLObjects(Scope.DEFAULT_ELEMENT_NAME).clear();
+ }
+
+ for (final Scope scope : scopes) {
+ if (GUARD_VALUE.equals(scope.getValue())) {
+ // Skip guard value for empty collections.
+ continue;
+ }
+
+ if (exts == null) {
+ // Need to add Extensions element.
+ exts = extBuilder.buildObject();
+ role.setExtensions(exts);
+ }
+
+ try {
+ log.info("Adding Scope '{}' to {} role in EntityDescriptor '{}'", scope.getValue(),
+ role.getElementQName(), entityID);
+ exts.getUnknownXMLObjects(Scope.DEFAULT_ELEMENT_NAME).add(XMLObjectSupport.cloneXMLObject(scope));
+ } catch (final MarshallingException | UnmarshallingException e) {
+ log.error("Error cloning Scope", e);
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/shib-metadata-impl/src/test/java/net/shibboleth/idp/saml/metadata/impl/ScopeFilterTest.java b/shib-metadata-impl/src/test/java/net/shibboleth/idp/saml/metadata/impl/ScopeFilterTest.java
new file mode 100644
index 00000000..63192a73
--- /dev/null
+++ b/shib-metadata-impl/src/test/java/net/shibboleth/idp/saml/metadata/impl/ScopeFilterTest.java
@@ -0,0 +1,196 @@
+/*
+ * Licensed 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.saml.metadata.impl;
+
+import java.io.File;
+import java.net.URL;
+import java.util.Collection;
+import java.util.List;
+import java.util.function.Predicate;
+
+import net.shibboleth.idp.saml.xmlobject.Scope;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+
+import org.opensaml.core.criterion.EntityIdCriterion;
+import org.opensaml.core.testing.XMLObjectBaseTestCase;
+import org.opensaml.core.xml.XMLObject;
+import org.opensaml.core.xml.XMLObjectBuilder;
+import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
+import org.opensaml.saml.metadata.resolver.impl.FilesystemMetadataResolver;
+import org.opensaml.saml.saml2.metadata.EntityDescriptor;
+import org.opensaml.saml.saml2.metadata.Extensions;
+import org.opensaml.saml.saml2.metadata.RoleDescriptor;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+ at SuppressWarnings("javadoc")
+public class ScopeFilterTest extends XMLObjectBaseTestCase implements Predicate<EntityDescriptor> {
+
+ private XMLObjectBuilder<Scope> scopeBuilder;
+
+ private FilesystemMetadataResolver metadataProvider;
+
+ private File mdFile;
+
+ private ScopeFilter metadataFilter;
+
+ private Collection<Scope> scopesToAdd;
+
+ @BeforeMethod
+ protected void setUp() throws Exception {
+
+ scopeBuilder =
+ XMLObjectProviderRegistrySupport.getBuilderFactory().<Scope>ensureBuilder(
+ Scope.DEFAULT_ELEMENT_NAME);
+
+ URL mdURL = ScopeFilterTest.class
+ .getResource("/net/shibboleth/idp/saml/impl/metadata/InCommon-metadata.xml");
+ mdFile = new File(mdURL.toURI());
+
+ metadataProvider = new FilesystemMetadataResolver(mdFile);
+ metadataProvider.setParserPool(parserPool);
+
+ metadataFilter = new ScopeFilter();
+
+ final Scope scope1 = scopeBuilder.buildObject(Scope.DEFAULT_ELEMENT_NAME);
+ scope1.setValue("scope1.org");
+ scope1.setRegexp(true);
+
+ final Scope scope2 = scopeBuilder.buildObject(Scope.DEFAULT_ELEMENT_NAME);
+ scope2.setValue("scope2.org");
+
+ scopesToAdd = CollectionSupport.listOf(scope1, scope2);
+ }
+
+ @Test
+ public void test() throws ComponentInitializationException, ResolverException {
+
+ metadataFilter.setRules(CollectionSupport.singletonMap(this, scopesToAdd));
+ metadataFilter.initialize();
+
+ metadataProvider.setMetadataFilter(metadataFilter);
+ metadataProvider.setId("test");
+ metadataProvider.initialize();
+
+ final EntityIdCriterion key = new EntityIdCriterion("urn:mace:incommon:osu.edu");
+ EntityDescriptor entity = metadataProvider.resolveSingle(new CriteriaSet(key));
+ assert entity != null;
+
+ for (final RoleDescriptor role : entity.getRoleDescriptors()) {
+ final Extensions exts = role.getExtensions();
+ assert exts != null;
+ final List<XMLObject> scopes = exts.getUnknownXMLObjects(Scope.DEFAULT_ELEMENT_NAME);
+ Assert.assertEquals(scopes.size(), 3);
+ if (scopes.get(0) instanceof Scope s) {
+ Assert.assertEquals(s.getValue(), "osu.edu");
+ Assert.assertEquals(s.getRegexp(), false);
+ } else {
+ Assert.fail("Scope was of unexpected type");
+ }
+ if (scopes.get(1) instanceof Scope s) {
+ Assert.assertEquals(s.getValue(), "scope1.org");
+ Assert.assertEquals(s.getRegexp(), true);
+ } else {
+ Assert.fail("Scope was of unexpected type");
+ }
+ if (scopes.get(2) instanceof Scope s) {
+ Assert.assertEquals(s.getValue(), "scope2.org");
+ Assert.assertEquals(s.getRegexp(), false);
+ } else {
+ Assert.fail("Scope was of unexpected type");
+ }
+ }
+ }
+
+ @Test
+ public void testRemovalOnly() throws ComponentInitializationException, ResolverException {
+
+ metadataFilter.setRules(CollectionSupport.singletonMap(this, CollectionSupport.emptyList()));
+ metadataFilter.setRemoveExistingScopes(true);
+ metadataFilter.initialize();
+
+ metadataProvider.setMetadataFilter(metadataFilter);
+ metadataProvider.setId("test");
+ metadataProvider.initialize();
+
+ final EntityIdCriterion key = new EntityIdCriterion("urn:mace:incommon:osu.edu");
+ EntityDescriptor entity = metadataProvider.resolveSingle(new CriteriaSet(key));
+ assert entity != null;
+
+ Extensions exts = entity.getExtensions();
+ if (exts != null) {
+ Assert.assertTrue(exts.getUnknownXMLObjects(Scope.DEFAULT_ELEMENT_NAME).isEmpty());
+ }
+
+ for (final RoleDescriptor role : entity.getRoleDescriptors()) {
+ exts = role.getExtensions();
+ if (exts != null) {
+ Assert.assertTrue(exts.getUnknownXMLObjects(Scope.DEFAULT_ELEMENT_NAME).isEmpty());
+ }
+ }
+
+ }
+
+ @Test
+ public void testWithRemoval() throws ComponentInitializationException, ResolverException {
+
+ metadataFilter.setRules(CollectionSupport.singletonMap(this, scopesToAdd));
+ metadataFilter.setRemoveExistingScopes(true);
+ metadataFilter.initialize();
+
+ metadataProvider.setMetadataFilter(metadataFilter);
+ metadataProvider.setId("test");
+ metadataProvider.initialize();
+
+ final EntityIdCriterion key = new EntityIdCriterion("urn:mace:incommon:osu.edu");
+ final EntityDescriptor entity = metadataProvider.resolveSingle(new CriteriaSet(key));
+ assert entity != null;
+
+ Extensions exts = entity.getExtensions();
+ if (exts != null) {
+ Assert.assertTrue(exts.getUnknownXMLObjects(Scope.DEFAULT_ELEMENT_NAME).isEmpty());
+ }
+
+ for (final RoleDescriptor role : entity.getRoleDescriptors()) {
+ exts = role.getExtensions();
+ assert exts != null;
+ final List<XMLObject> scopes = exts.getUnknownXMLObjects(Scope.DEFAULT_ELEMENT_NAME);
+ Assert.assertEquals(scopes.size(), 2);
+ if (scopes.get(0) instanceof Scope s) {
+ Assert.assertEquals(s.getValue(), "scope1.org");
+ Assert.assertEquals(s.getRegexp(), true);
+ } else {
+ Assert.fail("Scope was of unexpected type");
+ }
+ if (scopes.get(1) instanceof Scope s) {
+ Assert.assertEquals(s.getValue(), "scope2.org");
+ Assert.assertEquals(s.getRegexp(), false);
+ } else {
+ Assert.fail("Scope was of unexpected type");
+ }
+ }
+
+ }
+
+ /** {@inheritDoc} */
+ public boolean test(EntityDescriptor input) {
+ return "urn:mace:incommon:osu.edu".equals(input.getEntityID());
+ }
+
+}
\ No newline at end of file
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list