[java-shib-attribute] branch main updated: JSATTR-6: SAML AttributeQuery DataConnector
Brent Putman
putmanb at georgetown.edu
Tue Jun 3 04:50:39 UTC 2025
This is an automated email from the git hooks/post-receive script.
putmanb pushed a commit to branch main
in repository java-shib-attribute.
View the commit online:
http://git.shibboleth.net/view/?p=java-shib-attribute.git;a=commit;h=7e5878092955d7ec547e74deea914d36aae5f92e
The following commit(s) were added to refs/heads/main by this push:
new 7e5878092 JSATTR-6: SAML AttributeQuery DataConnector
7e5878092 is described below
commit 7e5878092955d7ec547e74deea914d36aae5f92e
Author: Brent Putman <putmanb at georgetown.edu>
AuthorDate: Thu Sep 19 23:24:21 2024 -0400
JSATTR-6: SAML AttributeQuery DataConnector
Initial commit of SAML DataConnector components.
---
pom.xml | 1 +
shib-attribute-resolver-api/pom.xml | 4 +
.../resolver/dc/saml/ExecutableQuery.java | 44 ++
.../attribute/resolver/dc/saml/ResponseData.java | 112 ++++
.../resolver/dc/saml/ResponseMappingStrategy.java | 27 +
.../attribute/resolver/dc/saml/package-info.java | 21 +
shib-attribute-resolver-impl/pom.xml | 23 +-
.../impl/AttributeAuthorityEntityIDResolver.java | 37 ++
.../dc/saml/impl/BasicResponseMappingStrategy.java | 458 ++++++++++++++
.../dc/saml/impl/ExecutableQueryBuilder.java | 658 +++++++++++++++++++++
.../resolver/dc/saml/impl/SAMLDataConnector.java | 253 ++++++++
.../dc/saml/impl/SelfEntityIDResolver.java | 40 ++
.../impl/SimpleAggregationSAMLDataConnector.java | 203 +++++++
.../resolver/dc/saml/impl/SubjectResolver.java | 41 ++
.../resolver/dc/saml/impl/package-info.java | 22 +
.../impl/ChainingAuthorityEntityIDResolver.java | 106 ++++
.../plugin/impl/ChainingSelfEntityIDResolver.java | 108 ++++
.../saml/plugin/impl/ChainingSubjectResolver.java | 108 ++++
.../impl/ContextAuthorityEntityIDResolver.java | 50 ++
.../CriteriaDecryptionConfigurationLookup.java | 124 ++++
.../plugin/impl/CriteriaSelfEntityIDResolver.java | 101 ++++
.../impl/DependencyAttributeSubjectResolver.java | 249 ++++++++
.../resolver/dc/saml/plugin/impl/package-info.java | 21 +
.../util/impl/AssertionValidationProcessor.java | 222 +++++++
.../impl/AttributeAuthorityEntityIDContext.java | 53 ++
.../impl/AttributeAuthorityEntityIDReference.java | 34 ++
.../impl/AttributeAuthorityEntityIDSource.java | 49 ++
.../util/impl/AttributeAuthorityEntityIDValue.java | 34 ++
.../dc/saml/util/impl/DecryptionProcessor.java | 367 ++++++++++++
.../dc/saml/util/impl/SubjectMatchProcessor.java | 89 +++
.../resolver/dc/saml/util/impl/package-info.java | 21 +
31 files changed, 3670 insertions(+), 10 deletions(-)
diff --git a/pom.xml b/pom.xml
index 4ecdd2bdd..7d3a8624d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -48,6 +48,7 @@
<opensaml.version>5.2.0-SNAPSHOT</opensaml.version>
<shib-metadata.groupId>net.shibboleth</shib-metadata.groupId>
<shib-metadata.version>5.2.0-SNAPSHOT</shib-metadata.version>
+
<checkstyle.configLocation>${project.basedir}/resources/checkstyle/checkstyle.xml</checkstyle.configLocation>
</properties>
diff --git a/shib-attribute-resolver-api/pom.xml b/shib-attribute-resolver-api/pom.xml
index 25620aef7..5834d6bc1 100644
--- a/shib-attribute-resolver-api/pom.xml
+++ b/shib-attribute-resolver-api/pom.xml
@@ -32,6 +32,10 @@
<groupId>${opensaml.groupId}</groupId>
<artifactId>opensaml-messaging-api</artifactId>
</dependency>
+ <dependency>
+ <groupId>${opensaml.groupId}</groupId>
+ <artifactId>opensaml-saml-api</artifactId>
+ </dependency>
<dependency>
<groupId>${opensaml.groupId}</groupId>
<artifactId>opensaml-profile-api</artifactId>
diff --git a/shib-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/ExecutableQuery.java b/shib-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/ExecutableQuery.java
new file mode 100644
index 000000000..7bf3adddf
--- /dev/null
+++ b/shib-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/ExecutableQuery.java
@@ -0,0 +1,44 @@
+/*
+ * 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.attribute.resolver.dc.saml;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.saml.common.SAMLException;
+import org.opensaml.security.SecurityException;
+import org.opensaml.soap.client.SOAPClient;
+import org.opensaml.soap.common.SOAPException;
+
+import net.shibboleth.idp.attribute.resolver.dc.ExecutableSearch;
+
+/** A query that can be executed against a SAML 2 AttributeAuthority to fetch results. */
+public interface ExecutableQuery extends ExecutableSearch {
+
+ /**
+ * Executes the query and returns the results.
+ *
+ * @param soapClient SOAP client to process the query
+ *
+ * @return the result of the executed query
+ *
+ * @throws SAMLException if there is a SAML error during resolution
+ * @throws SOAPException if there is a SOAP error during resolution
+ * @throws SecurityException if there there is a security-related error during resolution
+ *
+ */
+ @Nonnull ResponseData execute(@Nonnull SOAPClient soapClient)
+ throws SAMLException, SOAPException, SecurityException;
+
+}
\ No newline at end of file
diff --git a/shib-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/ResponseData.java b/shib-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/ResponseData.java
new file mode 100644
index 000000000..9c81e40c5
--- /dev/null
+++ b/shib-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/ResponseData.java
@@ -0,0 +1,112 @@
+/*
+ * 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.attribute.resolver.dc.saml;
+
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.InOutOperationContext;
+import org.opensaml.saml.saml2.core.Response;
+import org.opensaml.saml.saml2.metadata.RoleDescriptor;
+
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * A class which encapsulates the SAML 2 {@link Response} to an attribute query and associated data
+ * used in processing it within a data connector.
+ */
+public class ResponseData {
+
+ /** Response. */
+ @Nonnull private Response response;
+
+ /** SOAP client operation context. */
+ @Nonnull private InOutOperationContext soapClientContext;
+
+ /** Attribute resolution context. */
+ @Nonnull private AttributeResolutionContext attributeResolutionContext;
+
+ /** Dependency attributes. */
+ @Nonnull private Map<String, List<IdPAttributeValue>> dependencyAttributes;
+
+ /** Attribute authority role descriptor. */
+ @Nonnull private RoleDescriptor attributeAuthorityRoleDescriptor;
+
+
+ /**
+ * Constructor.
+ *
+ * @param message SAML 2 Response message
+ * @param soapContext SOAP client operation context
+ * @param attributeContext attribute resolution context
+ * @param dependencies resolution dependency attributes
+ * @param roleDescriptor attribute authority role descriptor
+ */
+ public ResponseData(@Nonnull final Response message, @Nonnull final InOutOperationContext soapContext,
+ @Nonnull final AttributeResolutionContext attributeContext,
+ @Nonnull final Map<String, List<IdPAttributeValue>> dependencies,
+ @Nonnull final RoleDescriptor roleDescriptor) {
+ response = Constraint.isNotNull(message, "Response message was null");
+ soapClientContext = Constraint.isNotNull(soapContext, "SOAP client operation context was null");
+ attributeResolutionContext = Constraint.isNotNull(attributeContext, "Attribute resolution context was null");
+ dependencyAttributes = Constraint.isNotNull(dependencies, "Dependency attributes were null");
+ attributeAuthorityRoleDescriptor = Constraint.isNotNull(roleDescriptor, "RoleDescriptor was null");
+ }
+
+ /**
+ * Get the SAML 2 {@link Response}.
+ *
+ * @return the SAML 2 Response message
+ */
+ @Nonnull public Response getResponse() {
+ return response;
+ }
+
+ /**
+ * Get the SOAP client {@link InOutOperationContext}.
+ *
+ * @return the SOAP client operation context
+ */
+ @Nonnull public InOutOperationContext getSOAPClientContext() {
+ return soapClientContext;
+ }
+
+ /**
+ * Get the {@link AttributeResolutionContext}.
+ *
+ * @return the attribute resolution context
+ */
+ @Nonnull public AttributeResolutionContext getAttributeResolutionContext() {
+ return attributeResolutionContext;
+ }
+
+ /**
+ * Get the resolution dependency attributes.
+ *
+ * @return Returns the dependency attributes
+ */
+ @Nonnull public Map<String, List<IdPAttributeValue>> getDependencyAttributes() {
+ return dependencyAttributes;
+ }
+
+ @Nonnull public RoleDescriptor getAttributeAuthorityRoleDescriptor() {
+ return attributeAuthorityRoleDescriptor;
+ }
+
+}
diff --git a/shib-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/ResponseMappingStrategy.java b/shib-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/ResponseMappingStrategy.java
new file mode 100644
index 000000000..b4034f083
--- /dev/null
+++ b/shib-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/ResponseMappingStrategy.java
@@ -0,0 +1,27 @@
+/*
+ * 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.attribute.resolver.dc.saml;
+
+import org.opensaml.saml.saml2.core.Response;
+
+import net.shibboleth.idp.attribute.resolver.dc.MappingStrategy;
+
+/**
+ * Strategy for mapping from a {@link Response} to a collection of
+ * {@link net.shibboleth.idp.attribute.IdPAttribute}s.
+ */
+public interface ResponseMappingStrategy extends MappingStrategy<ResponseData> {
+
+}
\ No newline at end of file
diff --git a/shib-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/package-info.java b/shib-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/package-info.java
new file mode 100644
index 000000000..fa8117d5f
--- /dev/null
+++ b/shib-attribute-resolver-api/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * 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 for SAML data connector configuration.
+ */
+ at NonnullElements
+package net.shibboleth.idp.attribute.resolver.dc.saml;
+
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
diff --git a/shib-attribute-resolver-impl/pom.xml b/shib-attribute-resolver-impl/pom.xml
index 2dfe2746d..cf7d2d144 100644
--- a/shib-attribute-resolver-impl/pom.xml
+++ b/shib-attribute-resolver-impl/pom.xml
@@ -32,7 +32,16 @@
<artifactId>shib-attribute-resolver-api</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>shib-attribute-filter-api</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>${opensaml.groupId}</groupId>
+ <artifactId>opensaml-core-api</artifactId>
+ </dependency>
<dependency>
<groupId>${opensaml.groupId}</groupId>
<artifactId>opensaml-messaging-api</artifactId>
@@ -41,6 +50,10 @@
<groupId>${opensaml.groupId}</groupId>
<artifactId>opensaml-profile-api</artifactId>
</dependency>
+ <dependency>
+ <groupId>${opensaml.groupId}</groupId>
+ <artifactId>opensaml-saml-api</artifactId>
+ </dependency>
<dependency>
<groupId>${opensaml.groupId}</groupId>
<artifactId>opensaml-security-api</artifactId>
@@ -110,16 +123,6 @@
<scope>test</scope>
</dependency>
- <dependency>
- <groupId>${opensaml.groupId}</groupId>
- <artifactId>opensaml-core-api</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>${opensaml.groupId}</groupId>
- <artifactId>opensaml-saml-api</artifactId>
- <scope>test</scope>
- </dependency>
<dependency>
<!-- We need this even if mvn dependency:analyze says we don't - this
provides the implementations of the SAML interfaces and by definition cannot
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/AttributeAuthorityEntityIDResolver.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/AttributeAuthorityEntityIDResolver.java
new file mode 100644
index 000000000..8345be352
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/AttributeAuthorityEntityIDResolver.java
@@ -0,0 +1,37 @@
+/*
+ * 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.attribute.resolver.dc.saml.impl;
+
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext;
+
+/**
+ * Interface for a component which resolves the entityID of an AttributeAuthority
+ * against which to query for attributes.
+ */
+public interface AttributeAuthorityEntityIDResolver {
+
+ @Nullable
+ String resolve(@Nonnull final AttributeResolutionContext resolutionContext,
+ @Nonnull final Map<String, List<IdPAttributeValue>> dependencyAttributes) throws ResolutionException;
+
+}
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/BasicResponseMappingStrategy.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/BasicResponseMappingStrategy.java
new file mode 100644
index 000000000..e494be73e
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/BasicResponseMappingStrategy.java
@@ -0,0 +1,458 @@
+/*
+ * 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.attribute.resolver.dc.saml.impl;
+
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.core.criterion.EntityIdCriterion;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.saml.common.messaging.context.SAMLMetadataContext;
+import org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext;
+import org.opensaml.saml.common.messaging.context.SAMLSelfEntityContext;
+import org.opensaml.saml.common.xml.SAMLConstants;
+import org.opensaml.saml.criterion.EntityRoleCriterion;
+import org.opensaml.saml.criterion.ProtocolCriterion;
+import org.opensaml.saml.metadata.resolver.MetadataResolver;
+import org.opensaml.saml.metadata.resolver.RoleDescriptorResolver;
+import org.opensaml.saml.saml2.core.Assertion;
+import org.opensaml.saml.saml2.core.Attribute;
+import org.opensaml.saml.saml2.core.AttributeStatement;
+import org.opensaml.saml.saml2.core.Issuer;
+import org.opensaml.saml.saml2.metadata.AttributeAuthorityDescriptor;
+import org.opensaml.saml.saml2.metadata.EntityDescriptor;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.IdPAttributeSupport;
+import net.shibboleth.idp.attribute.filter.AttributeFilter;
+import net.shibboleth.idp.attribute.filter.AttributeFilterException;
+import net.shibboleth.idp.attribute.filter.context.AttributeFilterContext;
+import net.shibboleth.idp.attribute.filter.context.AttributeFilterContext.Direction;
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.idp.attribute.resolver.dc.saml.ResponseData;
+import net.shibboleth.idp.attribute.resolver.dc.saml.ResponseMappingStrategy;
+import net.shibboleth.idp.attribute.transcoding.AttributeTranscoder;
+import net.shibboleth.idp.attribute.transcoding.AttributeTranscoderRegistry;
+import net.shibboleth.idp.attribute.transcoding.TranscoderSupport;
+import net.shibboleth.idp.attribute.transcoding.TranscodingRule;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.AbstractInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+import net.shibboleth.shared.service.ReloadableService;
+import net.shibboleth.shared.service.ServiceException;
+import net.shibboleth.shared.service.ServiceableComponent;
+
+/**
+ * Basic SAML 2 Response strategy to map to data connector output requirements.
+ */
+public class BasicResponseMappingStrategy extends AbstractInitializableComponent implements ResponseMappingStrategy {
+
+ /** Logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(BasicResponseMappingStrategy.class);
+
+ /** Transcoder registry. */
+ @NonnullAfterInit private AttributeTranscoderRegistry transcoderRegistry;
+
+ /** MetadataResolver. */
+ @NonnullAfterInit private MetadataResolver metadataResolver;
+
+ /** Role descriptor resolver. */
+ @NonnullAfterInit private RoleDescriptorResolver roleDescriptorResolver;
+
+ /** AttributeFilter service. */
+ @NonnullAfterInit private ReloadableService<AttributeFilter> filterService;
+
+ /** Flag indicating whether to filter attributes. */
+ private boolean filterAttributes = true;
+
+ /**
+ * Set the instance of {@link AttributeTranscoderRegistry} to use.
+ *
+ * @param registry the transcoder registry
+ */
+ public void setTranscoderRegistry(final @Nullable AttributeTranscoderRegistry registry) {
+ checkSetterPreconditions();
+ transcoderRegistry = registry;
+ }
+
+ /**
+ * Set the instance of {@link MetadataResolver} to use.
+ *
+ * @param resolver the metadata resolver
+ */
+ public void setMetadataResolver(final @Nullable MetadataResolver resolver) {
+ checkSetterPreconditions();
+ metadataResolver = resolver;
+ }
+
+ /**
+ * Set the role descriptor resolver.
+ *
+ * @param resolver the role descriptor resolver
+ */
+ public void setRoleDescriptorResolver(@Nullable final RoleDescriptorResolver resolver) {
+ checkSetterPreconditions();
+ roleDescriptorResolver = resolver;
+ }
+
+ /**
+ * Set the instance of {@link AttributeFilter} service to use.
+ *
+ * @param service the attribute filter service
+ */
+ public void setAttributeFilterService(final @Nullable ReloadableService<AttributeFilter> service) {
+ checkSetterPreconditions();
+ filterService = service;
+ }
+
+ /**
+ * Set the flag indicating whether to filter attributes.
+ *
+ * @param flag true if attributes should be filter, otherwise false
+ */
+ public void setFilterAttributes(final boolean flag) {
+ filterAttributes = flag;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (transcoderRegistry == null) {
+ throw new ComponentInitializationException("AttributeTranscoderRegistry was null");
+ }
+ if (metadataResolver == null) {
+ throw new ComponentInitializationException("MetadataResolver was null");
+ }
+ if (roleDescriptorResolver == null) {
+ throw new ComponentInitializationException("RoleDescriptorResolver was null");
+ }
+ if (filterService == null) {
+ throw new ComponentInitializationException("AttributeFilter service was null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nullable
+ public Map<String, IdPAttribute> map(@Nonnull final ResponseData responseData) throws ResolutionException {
+ checkComponentActive();
+
+ final List<IdPAttribute> aggregateResults = new LinkedList<>();
+
+ final ProfileRequestContext profileContext = responseData.getAttributeResolutionContext()
+ .getProfileRequestContextLookupStrategy()
+ .apply(responseData.getAttributeResolutionContext());
+ if (profileContext != null) {
+ log.trace("For transcoding successfully resolved ProfileRequestContext");
+ } else {
+ log.trace("For transcoding failed to resolve ProfileRequestContext, some features may be unavailable");
+ }
+
+ for (final Assertion assertion : responseData.getResponse().getAssertions()) {
+ String assertionIssuerID = null;
+ final Issuer assertionIssuer = assertion.getIssuer();
+ if (assertionIssuer != null) {
+ assertionIssuerID = assertionIssuer.getValue();
+ }
+
+ log.debug("Processing attributes from Assertion '{}' issued by '{}'",
+ assertion.getID(), assertionIssuerID != null ? assertionIssuerID : "<unknown>");
+
+ final List<IdPAttribute> assertionResults = new LinkedList<>();
+
+ for (final AttributeStatement statement : assertion.getAttributeStatements()) {
+ for (final Attribute samlAttribute : statement.getAttributes()) {
+ assert samlAttribute != null;
+ assertionResults.addAll(transcodeAttribute(samlAttribute, profileContext));
+ }
+ }
+
+ aggregateResults.addAll(processAssertionFiltering(assertion, assertionResults, assertionIssuerID,
+ profileContext, responseData));
+ }
+
+ final Map<String,IdPAttribute> mergedResults = IdPAttributeSupport.toMapMergeDuplicates(aggregateResults);
+
+ if (log.isTraceEnabled()) {
+ log.trace("Decoded {} total unique attribute IDs:", mergedResults.keySet().size());
+ for (final String attributeID : mergedResults.keySet().stream().sorted().toList()) {
+ log.trace("\tAttribute ID '{}', # of values: {}",
+ attributeID, mergedResults.get(attributeID).getValues().size());
+ }
+ }
+
+ return mergedResults;
+ }
+
+ /**
+ * Transcode a specified {@link Attribute} into a list of {@link IdPAttribute}.
+ *
+ * @param samlAttribute the SAML Attribute to transcode
+ * @param profileContext the profile request context being processed, if available
+ *
+ * @return collection of transcoded attributes
+ */
+ @Nonnull private List<IdPAttribute> transcodeAttribute(@Nonnull final Attribute samlAttribute,
+ @Nullable final ProfileRequestContext profileContext) {
+
+ final List<IdPAttribute> attributeResults = new LinkedList<>();
+
+ final Collection<TranscodingRule> transcodingRules = transcoderRegistry.getTranscodingRules(samlAttribute);
+ if (transcodingRules.isEmpty()) {
+ log.debug("No transcoding rule for Attribute (Name '{}', NameFormat: '{}')",
+ samlAttribute.getName(),
+ samlAttribute.getNameFormat() != null ? samlAttribute.getNameFormat()
+ : Attribute.UNSPECIFIED);
+ return CollectionSupport.emptyList();
+ }
+
+ for (final TranscodingRule rule : transcodingRules) {
+ assert rule != null;
+
+ final AttributeTranscoder<Attribute> transcoder = TranscoderSupport.getTranscoder(rule);
+ try {
+ final IdPAttribute decodedAttribute = transcoder.decode(profileContext, samlAttribute, rule);
+ if (decodedAttribute != null) {
+ attributeResults.add(decodedAttribute);
+ }
+ } catch (final Exception e) {
+ log.warn("Error transcoding for Attribute (Name '{}', NameFormat: '{}') via transcoder: {}",
+ samlAttribute.getName(),
+ samlAttribute.getNameFormat() != null ? samlAttribute.getNameFormat()
+ : Attribute.UNSPECIFIED,
+ transcoder.getClass().getName(),
+ e);
+ }
+ }
+
+ return attributeResults;
+ }
+
+ /**
+ * Process filtering the attributes of an Assertion.
+ *
+ * @param assertion the assertion being processed
+ * @param assertionResults the decoded attributes of an Assertion
+ * @param assertionIssuerID the Assertion issuer ID
+ * @param profileContext the profile request being processed
+ * @param responseData the Response data being processed
+ *
+ * @return the results of filtering for the assertion's attributes
+ */
+ @Nonnull
+ private Collection<IdPAttribute> processAssertionFiltering(@Nonnull final Assertion assertion,
+ @Nonnull final List<IdPAttribute> assertionResults, @Nullable final String assertionIssuerID,
+ @Nullable final ProfileRequestContext profileContext, @Nonnull final ResponseData responseData) {
+
+ if (filterAttributes) {
+ if (assertionIssuerID != null && profileContext != null) {
+ log.debug("Filtering attributes decoded from Assertion '{}' issued by '{}'",
+ assertion.getID(), assertionIssuerID);
+ return filterAssertionResults(assertionResults, assertionIssuerID, profileContext, responseData);
+
+ } else {
+ log.error("Attribute filtering enabled, but Assertion Issuer or ProfileRequestContext unknown, "
+ + " unable to filter and include attributes in result set from Assertion '{}'",
+ assertion.getID());
+ return CollectionSupport.emptyList();
+ }
+ } else {
+ log.debug("Attribute filtering disabled, adding all attributes from Assertion '{}' issued by '{}'",
+ assertion.getID(), assertionIssuerID != null ? assertionIssuerID : "<unknown>");
+ return assertionResults;
+ }
+ }
+
+ /**
+ * Filter the decoded attributes from an Assertion.
+ *
+ * @param assertionResults the decoded attributes of an Assertion
+ * @param assertionIssuerID the Assertion issuer ID
+ * @param profileContext the profile request being processed
+ * @param responseData the Response data being processed
+ *
+ * @return the filtered attributes
+ */
+ @Nonnull private Collection<IdPAttribute> filterAssertionResults(@Nonnull final List<IdPAttribute> assertionResults,
+ @Nonnull final String assertionIssuerID, @Nonnull final ProfileRequestContext profileContext,
+ @Nonnull final ResponseData responseData) {
+
+ final AttributeFilterContext filterContext =
+ profileContext.ensureSubcontext(AttributeFilterContext.class);
+
+ final Map<String,IdPAttribute> unfilteredAttributes =
+ IdPAttributeSupport.toMapMergeDuplicates(assertionResults);
+
+ populateFilterContext(filterContext, unfilteredAttributes, assertionIssuerID, responseData);
+
+ try (final ServiceableComponent<AttributeFilter> component = filterService.getServiceableComponent()) {
+ final AttributeFilter filter = component.getComponent();
+ filter.filterAttributes(filterContext);
+ final Map<String,IdPAttribute> filtered = filterContext.getFilteredIdPAttributes();
+ if (filtered != null) {
+ final Collection<IdPAttribute> filteredValues = filtered.values();
+ if (filteredValues != null) {
+ return filteredValues;
+ }
+ }
+ return CollectionSupport.emptyList();
+ } catch (final AttributeFilterException e) {
+ log.error("Error while filtering inbound attributes", e);
+ return CollectionSupport.emptyList();
+ } catch (final ServiceException e) {
+ log.error("Invalid AttributeFilter configuration", e);
+ return CollectionSupport.emptyList();
+ } finally {
+ filterContext.removeFromParent();
+ }
+
+ }
+
+ /**
+ * Populate the {@link AttributeFilterContext} for processing.
+ *
+ * @param filterContext the filter context to populate
+ * @param unfilteredAttributes the unfiltered attributes
+ * @param assertionIssuer issuer of the Assertion being processed
+ * @param responseData the response data being processed
+ */
+ private void populateFilterContext(@Nonnull final AttributeFilterContext filterContext,
+ @Nonnull final Map<String, IdPAttribute> unfilteredAttributes,
+ @Nonnull final String assertionIssuer,
+ @Nonnull final ResponseData responseData) {
+
+ final SAMLMetadataContext issuerMetadataContext = resolveIssuerMetadataContext(responseData, assertionIssuer);
+ final String selfEntityID = resolveSelfEntityID(responseData);
+
+ filterContext.setDirection(Direction.INBOUND)
+ .setPrefilteredIdPAttributes(unfilteredAttributes)
+ .setMetadataResolver(metadataResolver)
+ .setIssuerMetadataContextLookupStrategy(t -> issuerMetadataContext)
+ .setAttributeIssuerID(assertionIssuer)
+ .setAttributeRecipientID(selfEntityID);
+ }
+
+ /**
+ * Resolve the self entityID used for the original attribute query.
+ *
+ * @param responseData the response data being processed
+ *
+ * @return the self entityID
+ */
+ @Nullable private String resolveSelfEntityID(@Nonnull final ResponseData responseData) {
+ return responseData
+ .getSOAPClientContext()
+ .ensureSubcontext(SAMLSelfEntityContext.class)
+ .getEntityId();
+ }
+
+ /**
+ * Resolve the {@link SAMLMetadataContext} for the assertion issuer.
+ *
+ * @param responseData the response data being processed
+ * @param assertionIssuer issuer of the Assertion being processed
+ *
+ * @return the resolved SAMLMetadataContext instance
+ */
+ @Nonnull private SAMLMetadataContext resolveIssuerMetadataContext(@Nonnull final ResponseData responseData,
+ @Nonnull final String assertionIssuer) {
+
+ // First check if Assertion issuer is the same as Response issuer.
+ // If so, use the SAMLMetadataContext that already exists
+ final SAMLPeerEntityContext peerEntityContext = resolvePeerEntityContext(responseData);
+ if (assertionIssuer.equals(peerEntityContext.getEntityId())) {
+ final SAMLMetadataContext peerMetadataContext =
+ peerEntityContext.getSubcontext(SAMLMetadataContext.class);
+ if (peerMetadataContext != null
+ && peerMetadataContext.getEntityDescriptor() != null
+ && peerMetadataContext.getRoleDescriptor() != null) {
+ log.debug("Assertion issuer same as Response, resolved SAMLMetadataContext from SOAP client context");
+ return peerMetadataContext;
+ } else {
+ log.debug("Assertion issuer same as Response but context data incomplete, "
+ + "resolving data for SAMLMetadataContext");
+ }
+ } else {
+ log.debug("Assertion issuer NOT same as Response, resolving data for SAMLMetadataContext");
+ }
+
+
+ // It didn't exist so resolve data via resolver
+ final SAMLMetadataContext metadataContext = new SAMLMetadataContext();
+
+ final AttributeAuthorityDescriptor roleDescriptor = resolveAuthorityRoleDescriptor(assertionIssuer);
+ metadataContext.setRoleDescriptor(roleDescriptor);
+ if (roleDescriptor != null && roleDescriptor.getParent() instanceof EntityDescriptor entityDescriptor) {
+ metadataContext.setEntityDescriptor(entityDescriptor);
+ }
+
+ return metadataContext;
+ }
+
+ /**
+ * Resolve the response issuer's {@link SAMLPeerEntityContext}.
+ *
+ * @param responseData the response data being processed
+ *
+ * @return the self entityID
+ */
+ @Nonnull private SAMLPeerEntityContext resolvePeerEntityContext(@Nonnull final ResponseData responseData) {
+ return responseData
+ .getSOAPClientContext()
+ .ensureSubcontext(SAMLPeerEntityContext.class);
+ }
+
+ /**
+ * Resolve the attribute authority role descriptor.
+ *
+ * @param authorityEntityID the entityID of the attribute authority being processed
+ *
+ * @return the the role descriptor of the attribute authority being processed, or null if not available
+ */
+ @Nullable private AttributeAuthorityDescriptor resolveAuthorityRoleDescriptor(
+ @Nonnull final String authorityEntityID) {
+
+ final CriteriaSet criteriaSet = new CriteriaSet(
+ new EntityIdCriterion(authorityEntityID),
+ new ProtocolCriterion(SAMLConstants.SAML20P_NS),
+ new EntityRoleCriterion(AttributeAuthorityDescriptor.DEFAULT_ELEMENT_NAME));
+ try {
+ final AttributeAuthorityDescriptor descriptor =
+ (AttributeAuthorityDescriptor) roleDescriptorResolver.resolveSingle(criteriaSet);
+ if (descriptor != null) {
+ log.debug("Successfully resolved AttributeAuthorityDescriptor for entityID: {}", authorityEntityID);
+ return descriptor;
+ }
+ log.warn("Failed to resolve AttributeAuthorityDescriptor for entityID: {}", authorityEntityID);
+ return null;
+ } catch (final ResolverException e) {
+ log.warn("Fatal error resolving AttributeAuthorityDescriptor for entityID: {}", authorityEntityID, e);
+ return null;
+ }
+ }
+
+}
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/ExecutableQueryBuilder.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/ExecutableQueryBuilder.java
new file mode 100644
index 000000000..1776725a2
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/ExecutableQueryBuilder.java
@@ -0,0 +1,658 @@
+/*
+ * 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.attribute.resolver.dc.saml.impl;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.core.criterion.EntityIdCriterion;
+import org.opensaml.core.xml.io.MarshallingException;
+import org.opensaml.core.xml.io.UnmarshallingException;
+import org.opensaml.core.xml.util.XMLObjectSupport;
+import org.opensaml.messaging.MessageException;
+import org.opensaml.messaging.context.InOutOperationContext;
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.saml.common.SAMLException;
+import org.opensaml.saml.common.SAMLObject;
+import org.opensaml.saml.common.binding.EndpointResolver;
+import org.opensaml.saml.common.messaging.soap.SAMLSOAPClientContextBuilder;
+import org.opensaml.saml.common.xml.SAMLConstants;
+import org.opensaml.saml.criterion.EndpointCriterion;
+import org.opensaml.saml.criterion.EntityRoleCriterion;
+import org.opensaml.saml.criterion.ProtocolCriterion;
+import org.opensaml.saml.criterion.RoleDescriptorCriterion;
+import org.opensaml.saml.metadata.resolver.RoleDescriptorResolver;
+import org.opensaml.saml.saml2.core.Attribute;
+import org.opensaml.saml.saml2.core.AttributeQuery;
+import org.opensaml.saml.saml2.core.Issuer;
+import org.opensaml.saml.saml2.core.NameID;
+import org.opensaml.saml.saml2.core.Response;
+import org.opensaml.saml.saml2.core.Status;
+import org.opensaml.saml.saml2.core.StatusCode;
+import org.opensaml.saml.saml2.core.Subject;
+import org.opensaml.saml.saml2.metadata.AttributeAuthorityDescriptor;
+import org.opensaml.saml.saml2.metadata.AttributeService;
+import org.opensaml.security.SecurityException;
+import org.opensaml.soap.client.SOAPClient;
+import org.opensaml.soap.client.http.PipelineFactoryHttpSOAPClient;
+import org.opensaml.soap.common.SOAPException;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext;
+import net.shibboleth.idp.attribute.resolver.dc.ExecutableSearchBuilder;
+import net.shibboleth.idp.attribute.resolver.dc.saml.ExecutableQuery;
+import net.shibboleth.idp.attribute.resolver.dc.saml.ResponseData;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.AbstractInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+import net.shibboleth.shared.security.IdentifierGenerationStrategy;
+
+/**
+ * Component which builds an {@link ExecutableQuery} used with the {@link SAMLDataConnector}.
+ */
+public class ExecutableQueryBuilder extends AbstractInitializableComponent
+ implements ExecutableSearchBuilder<ExecutableQuery> {
+
+ /** Logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(ExecutableQueryBuilder.class);
+
+ /** Resolver for AttributeService endpoints. **/
+ @NonnullAfterInit private EndpointResolver<AttributeService> authorityEndpointResolver;
+
+ /** Role descriptor resolver. */
+ @NonnullAfterInit private RoleDescriptorResolver roleDescriptorResolver;
+
+ /** The SOAP client message pipeline name. */
+ @NonnullAfterInit private String soapPipelineName;
+
+ /** Identifier generation strategy. */
+ @NonnullAfterInit private IdentifierGenerationStrategy idStrategy;
+
+ /** SOAP client security configuration profile ID. */
+ @NonnullAfterInit private String soapClientSecurityConfigurationProfileId;
+
+ /** Strategy for resolving the authority entityID. */
+ @NonnullAfterInit private AttributeAuthorityEntityIDResolver authorityEntityIDStrategy;
+
+ /** Strategy for resolving the authority entityID. */
+ @NonnullAfterInit private SelfEntityIDResolver selfEntityIDStrategy;
+
+ /** Strategy for resolving the authority entityID. */
+ @NonnullAfterInit private SubjectResolver subjectStrategy;
+
+ /** Optional list of saml2:Attributes to request in the query. */
+ @Nonnull private List<Attribute> requestedAttributes = CollectionSupport.emptyList();
+
+
+ /**
+ * Get the attribute authority endpoint resolver.
+ *
+ * @return the endpoint resolver
+ */
+ @NonnullAfterInit public EndpointResolver<AttributeService> getAuthorityEndpointResolver() {
+ return authorityEndpointResolver;
+ }
+
+ /**
+ * Set the attribute authority endpoint resolver.
+ *
+ * @param resolver the new resolver
+ */
+ public void setAuthorityEndpointResolver(@Nullable final EndpointResolver<AttributeService> resolver) {
+ checkSetterPreconditions();
+ authorityEndpointResolver = resolver;
+ }
+
+ /**
+ * Get the role descriptor resolver.
+ *
+ * @return the role descriptor resolver
+ */
+ @NonnullAfterInit public RoleDescriptorResolver getRoleDescriptorResolver() {
+ return roleDescriptorResolver;
+ }
+
+ /**
+ * Set the role descriptor resolver.
+ *
+ * @param resolver the role descriptor resolver
+ */
+ public void setRoleDescriptorResolver(@Nullable final RoleDescriptorResolver resolver) {
+ checkSetterPreconditions();
+ roleDescriptorResolver = resolver;
+ }
+
+ /**
+ * Get the name of the specific SOAP client message pipeline to use,
+ * for example with {@link PipelineFactoryHttpSOAPClient}.
+ *
+ * @return the pipeline name, or null
+ */
+ @NonnullAfterInit public String getSOAPPipelineName() {
+ return soapPipelineName;
+ }
+
+ /**
+ * Set the name of the specific SOAP client message pipeline to use,
+ * for example with {@link PipelineFactoryHttpSOAPClient}.
+ *
+ * @param name the pipeline name, or null
+ */
+ public void setSOAPPipelineName(@Nullable final String name) {
+ checkSetterPreconditions();
+ soapPipelineName = StringSupport.trimOrNull(name);
+ }
+
+ /**
+ * Get the identifier generation strategy.
+ *
+ * @return Returns the identifier generation strategy
+ */
+ @NonnullAfterInit public IdentifierGenerationStrategy getIdentifierGenerationStrategy() {
+ return idStrategy;
+ }
+
+ /**
+ * Set the identifier generation strategy.
+ *
+ * @param strategy the identifier generation strategy
+ */
+ public void setIdentifierGenerationStrategy(@Nonnull final IdentifierGenerationStrategy strategy) {
+ checkSetterPreconditions();
+ idStrategy = strategy;
+ }
+
+ /**
+ * Get the SOAP client security configuration profile ID to use.
+ *
+ * @return the client security configuration profile ID, or null
+ */
+ @NonnullAfterInit public String getSOAPClientSecurityConfigurationProfileId() {
+ return soapClientSecurityConfigurationProfileId;
+ }
+
+ /**
+ * Set the SOAP client security configuration profile ID to use.
+ *
+ * @param profileId the profile ID, or null
+ */
+ public void setSOAPClientSecurityConfigurationProfileId(@Nullable final String profileId) {
+ checkSetterPreconditions();
+ soapClientSecurityConfigurationProfileId = StringSupport.trimOrNull(profileId);
+ }
+
+ /**
+ * @return the strategy
+ */
+ @NonnullAfterInit public AttributeAuthorityEntityIDResolver getAuthorityEntityIDStrategy() {
+ return authorityEntityIDStrategy;
+ }
+
+ /**
+ * @param strategy the entityID resolution strategy
+ */
+ public void setAuthorityEntityIDStrategy(
+ @Nonnull final AttributeAuthorityEntityIDResolver strategy) {
+ checkSetterPreconditions();
+ authorityEntityIDStrategy = strategy;
+ }
+
+ /**
+ * @return the strategy
+ */
+ @NonnullAfterInit public SelfEntityIDResolver getSelfEntityIDStrategy() {
+ return selfEntityIDStrategy;
+ }
+
+ /**
+ * @param strategy the entityID resolution strategy
+ */
+ public void setSelfEntityIDStrategy(
+ @Nonnull final SelfEntityIDResolver strategy) {
+ checkSetterPreconditions();
+ selfEntityIDStrategy = strategy;
+ }
+
+ /**
+ * @return the strategy
+ */
+ @NonnullAfterInit public SubjectResolver getSubjectStrategy() {
+ return subjectStrategy;
+ }
+
+ /**
+ * @param strategy the entityID resolution strategy
+ */
+ public void setSubjectStrategy(
+ @Nonnull final SubjectResolver strategy) {
+ checkSetterPreconditions();
+ subjectStrategy = strategy;
+ }
+
+ /**
+ * Get the optional list of saml2:Attributes to request in the query.
+ *
+ * @return the list of attributes, may be null or empty
+ */
+ @Unmodifiable
+ @NotLive
+ @Nonnull public List<Attribute> getRequestedAttributes() {
+ return requestedAttributes;
+ }
+
+ /**
+ * Set the optional list of saml2:Attributes to request in the query.
+ *
+ * @param attributes the list of attributes to request, may be null or empty
+ */
+ public void setRequestedAttributes(@Nullable List<Attribute> attributes) {
+ if (attributes == null) {
+ requestedAttributes = CollectionSupport.emptyList();
+ } else {
+ requestedAttributes = attributes.stream()
+ .filter(Objects::nonNull)
+ .collect(CollectionSupport.nonnullCollector(Collectors.toUnmodifiableList())).get();
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (getSOAPClientSecurityConfigurationProfileId() == null) {
+ throw new ComponentInitializationException("SOAP client security config profile ID was null");
+ }
+ if (getAuthorityEndpointResolver() == null) {
+ throw new ComponentInitializationException("Authority EndpointResolver was null");
+ }
+ if (getAuthorityEntityIDStrategy() == null) {
+ throw new ComponentInitializationException("Authority entityID strategy was null");
+ }
+ if (getSelfEntityIDStrategy() == null) {
+ throw new ComponentInitializationException("Self entityID strategy was null");
+ }
+ if (getSubjectStrategy() == null) {
+ throw new ComponentInitializationException("Subject strategy was null");
+ }
+ if (getIdentifierGenerationStrategy() == null) {
+ throw new ComponentInitializationException("IdentifierGenerationStrategy was null");
+ }
+ if (getRoleDescriptorResolver() == null) {
+ throw new ComponentInitializationException("RoleDescriptorResolver was null");
+ }
+ if (getSOAPClientSecurityConfigurationProfileId() == null) {
+ throw new ComponentInitializationException("SOAP client security configuration profile ID was null");
+ }
+ if (getSOAPPipelineName() == null) {
+ throw new ComponentInitializationException("SOAP pipeline name was null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nonnull
+ public ExecutableQuery build(@Nonnull final AttributeResolutionContext resolutionContext,
+ @Nonnull final Map<String, List<IdPAttributeValue>> dependencyAttributes) throws ResolutionException {
+
+ checkComponentActive();
+
+ final String authorityEntityID = resolveAttributeAuthorityEntityID(resolutionContext, dependencyAttributes);
+ final AttributeAuthorityDescriptor authorityRoleDescriptor = resolveAuthorityRoleDescriptor(authorityEntityID);
+ final String authorityEndpoint = resolveAuthorityEndpoint(authorityEntityID, authorityRoleDescriptor);
+
+ final InOutOperationContext opContext = buildOperationContext(resolutionContext, dependencyAttributes,
+ authorityRoleDescriptor, authorityEndpoint);
+
+ return new ExecutableQuery() {
+
+ /** Logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(ExecutableQuery.class);
+
+ @Nullable public String getResultCacheKey() {
+ final MessageContext outbound = opContext.getOutboundMessageContext();
+ assert outbound != null;
+ final AttributeQuery query = (AttributeQuery) outbound.getMessage();
+ assert query != null;
+ final Subject subject = query.getSubject();
+ assert subject != null;
+ final NameID nameid = subject.getNameID();
+ assert nameid != null;
+ final List<String> attribNames = query.getAttributes().stream()
+ .map(Attribute::getName)
+ .sorted()
+ .toList();
+
+ final StringBuilder builder = new StringBuilder();
+
+ builder.append(authorityEntityID);
+ builder.append(":");
+ builder.append(nameid.getValue());
+ if (attribNames.size() > 0) {
+ builder.append(":");
+ builder.append(StringSupport.listToStringValue(attribNames, ","));
+ }
+
+ return builder.toString();
+ }
+
+ /** {@inheritDoc} */
+ public String toString() {
+ return getResultCacheKey();
+ }
+
+ @Nonnull public ResponseData execute(@Nonnull final SOAPClient soapClient)
+ throws SAMLException, SOAPException, SecurityException {
+
+ log.trace("Executing AttributeQuery over SOAP 1.1 binding to endpoint: {}", authorityEndpoint);
+ soapClient.send(authorityEndpoint, opContext);
+
+ final MessageContext inboundContext = opContext.getInboundMessageContext();
+ final Object message = inboundContext != null ? inboundContext.getMessage() : null;
+ if (message instanceof Response response) {
+ validateResponse(response);
+ return new ResponseData(response, opContext, resolutionContext, dependencyAttributes,
+ authorityRoleDescriptor);
+ }
+ throw new SOAPException("SOAP message payload was not an instance of Response: "
+ + (message != null ? message.getClass().getName() : "(null)"));
+ }
+
+ private void validateResponse(@Nonnull final Response response) throws SAMLException {
+ final Status status = response.getStatus();
+ final StatusCode statusCode = status != null ? status.getStatusCode() : null;
+ if (statusCode == null || statusCode.getValue() == null) {
+ throw new SAMLException("Response included no StatusCode, could not validate");
+ } else if (!StatusCode.SUCCESS.equals(statusCode.getValue())){
+ throw new SAMLException("Response carried non-success StatusCode: " + statusCode.getValue());
+ }
+ }
+ };
+ }
+
+ /**
+ * Resolve the attribute authority entityID.
+ *
+ * @param resolutionContext the current attribute resolution context
+ * @param dependencyAttributes the current set of dependency attributes
+ *
+ * @return the entityID of the attribute authority being processed
+ *
+ * @throws ResolutionException if there is a fatal error during processing
+ */
+ @Nonnull private String resolveAttributeAuthorityEntityID(@Nonnull final AttributeResolutionContext resolutionContext,
+ @Nonnull final Map<String, List<IdPAttributeValue>> dependencyAttributes) throws ResolutionException {
+
+ final String entityID = getAuthorityEntityIDStrategy().resolve(resolutionContext, dependencyAttributes);
+ if (entityID != null) {
+ return entityID;
+ }
+ throw new ResolutionException("Unable to resolve AttributeAuthority entityID");
+ }
+
+ /**
+ * Resolve the attribute authority role descriptor.
+ *
+ * @param authorityEntityID the entityID of the attribute authority being processed
+ *
+ * @return the the role descriptor of the attribute authority being processed
+ *
+ * @throws ResolutionException if there is a fatal error during processing
+ */
+ @Nonnull private AttributeAuthorityDescriptor resolveAuthorityRoleDescriptor(@Nonnull final String authorityEntityID)
+ throws ResolutionException {
+
+ final CriteriaSet criteriaSet = new CriteriaSet(
+ new EntityIdCriterion(authorityEntityID),
+ new ProtocolCriterion(SAMLConstants.SAML20P_NS),
+ new EntityRoleCriterion(AttributeAuthorityDescriptor.DEFAULT_ELEMENT_NAME));
+ try {
+ final AttributeAuthorityDescriptor descriptor =
+ (AttributeAuthorityDescriptor) roleDescriptorResolver.resolveSingle(criteriaSet);
+ if (descriptor != null) {
+ log.debug("Successfully resolved AttributeAuthorityDescriptor for entityID: {}", authorityEntityID);
+ return descriptor;
+ }
+ log.warn("Failed to resolve AttributeAuthorityDescriptor for entityID: {}", authorityEntityID);
+ throw new ResolutionException("Failed to resolve AttributeAuthorityDescriptor for entityID: "
+ + authorityEntityID);
+ } catch (final ResolverException e) {
+ log.warn("Fatal error resolving AttributeAuthorityDescriptor for entityID: {}", authorityEntityID, e);
+ throw new ResolutionException("Failed to resolve AttributeAuthorityDescriptor", e);
+ }
+
+ }
+
+ /**
+ * Resolve the attribute authority endpoint.
+ *
+ * @param authorityEntityID the entityID of the attribute authority being processed
+ * @param authorityRoleDescriptor the role descriptor of the attribute authority being processed
+ *
+ * @return the attribute authority endpoint
+ *
+ * @throws ResolutionException if there is a fatal error during processing
+ */
+ @Nonnull private String resolveAuthorityEndpoint(@Nonnull final String authorityEntityID,
+ @Nonnull final AttributeAuthorityDescriptor authorityRoleDescriptor) throws ResolutionException {
+
+ final RoleDescriptorCriterion roleDescriptorCriterion = new RoleDescriptorCriterion(authorityRoleDescriptor);
+
+ final AttributeService serviceTemplate =
+ (AttributeService) XMLObjectSupport.buildXMLObject(
+ AttributeService.DEFAULT_ELEMENT_NAME);
+ serviceTemplate.setBinding(SAMLConstants.SAML2_SOAP11_BINDING_URI);
+
+ final EndpointCriterion<AttributeService> endpointCriterion = new EndpointCriterion<>(serviceTemplate, false);
+
+ final CriteriaSet criteriaSet = new CriteriaSet(roleDescriptorCriterion, endpointCriterion);
+
+ try {
+ final AttributeService service = authorityEndpointResolver.resolveSingle(criteriaSet);
+ if (service != null) {
+ final String location = service.getLocation();
+ if (location != null) {
+ log.debug("Successfully resolved AttibuteService for entityID: {}", authorityEntityID);
+ return location;
+ }
+ log.warn("Failed to resolve AttibuteService Location for entityID: {}", authorityEntityID);
+ throw new ResolutionException("Failed to resolve AttibuteService Location for entityID: "
+ + authorityEntityID);
+ }
+ log.warn("Failed to resolve AttibuteService for entityID: {}", authorityEntityID);
+ throw new ResolutionException("Failed to resolve AttibuteService for entityID: " + authorityEntityID);
+ } catch (final ResolverException e) {
+ log.warn("Fatal error resolving AttributeService for entityID: {}", authorityEntityID, e);
+ throw new ResolutionException("Failed to resolve AttributeService", e);
+ }
+ }
+
+ /**
+ * Build the SOAP client operation context.
+ *
+ * @param resolutionContext the current attribute resolution context
+ * @param dependencyAttributes the current set of dependency attributes
+ * @param roleDescriptor the role descriptor of the attribute authority being processed
+ * @param endpoint the attribute authority endpoint
+ *
+ * @return the SOAP client operation context
+ *
+ * @throws ResolutionException if there is a fatal error during processing
+ */
+ @Nonnull private InOutOperationContext buildOperationContext(@Nonnull final AttributeResolutionContext resolutionContext,
+ @Nonnull final Map<String, List<IdPAttributeValue>> dependencyAttributes,
+ @Nonnull final AttributeAuthorityDescriptor roleDescriptor, @Nonnull final String endpoint)
+ throws ResolutionException {
+
+ final String selfEntityID = resolveSelfEntityID(resolutionContext, dependencyAttributes, roleDescriptor);
+
+ try {
+ return new SAMLSOAPClientContextBuilder<>()
+ .setOutboundMessage(buildAttributeQueryMessage(resolutionContext, dependencyAttributes, roleDescriptor,
+ endpoint, selfEntityID))
+ .setProtocol(SAMLConstants.SAML20P_NS)
+ .setPipelineName(getSOAPPipelineName())
+ .setPeerRoleDescriptor(roleDescriptor)
+ .setSelfEntityID(selfEntityID)
+ .setSecurityConfigurationProfileId(getSOAPClientSecurityConfigurationProfileId())
+ .build();
+ } catch (MessageException e) {
+ throw new ResolutionException("Fatal error building operation context for SAML AttributeQuery", e);
+ }
+ }
+
+ /**
+ * Resolve the entity ID of the protocol message issuer (this entity).
+ *
+ * @param resolutionContext the current attribute resolution context
+ * @param dependencyAttributes the current set of dependency attributes
+ * @param roleDescriptor the role descriptor of the attribute authority being processed
+ *
+ * @return the entity ID of the protocol message issuer (this entity)
+ *
+ * @throws ResolutionException if there is a fatal error during processing
+ */
+ @Nonnull private String resolveSelfEntityID(@Nonnull final AttributeResolutionContext resolutionContext,
+ @Nonnull final Map<String, List<IdPAttributeValue>> dependencyAttributes,
+ @Nonnull final AttributeAuthorityDescriptor roleDescriptor) throws ResolutionException {
+
+ final String entityID = getSelfEntityIDStrategy().resolve(resolutionContext, dependencyAttributes,
+ roleDescriptor);
+ if (entityID != null) {
+ return entityID;
+ }
+ throw new ResolutionException("Unable to resolve self entityID");
+ }
+
+ /**
+ * Build the attribute query message to be sent to the attribute authority.
+ *
+ * @param resolutionContext the current attribute resolution context
+ * @param dependencyAttributes the current set of dependency attributes
+ * @param roleDescriptor the role descriptor of the attribute authority being processed
+ * @param endpoint the attribute authority endpoint
+ * @param selfEntityID the entity ID of the protocol message issuer (this entity)
+ *
+ * @return the new attribute query message to be sent to the attribute authority
+ *
+ * @throws ResolutionException if there is a fatal error during processing
+ */
+ @Nonnull private SAMLObject buildAttributeQueryMessage(@Nonnull final AttributeResolutionContext resolutionContext,
+ @Nonnull final Map<String, List<IdPAttributeValue>> dependencyAttributes,
+ @Nonnull final AttributeAuthorityDescriptor roleDescriptor,
+ @Nonnull final String endpoint, @Nonnull final String selfEntityID) throws ResolutionException {
+
+ final AttributeQuery query =
+ (AttributeQuery) XMLObjectSupport.buildXMLObject(AttributeQuery.DEFAULT_ELEMENT_NAME);
+
+ query.setID(idStrategy.generateIdentifier(true));
+ query.setDestination(endpoint);
+ query.setIssueInstant(Instant.now());
+ query.setIssuer(buildIssuer(selfEntityID));
+
+ query.setSubject(buildSubject(resolutionContext, dependencyAttributes, roleDescriptor));
+
+ query.getAttributes().addAll(buildRequestAttributes(resolutionContext, dependencyAttributes, roleDescriptor));
+
+ return query;
+ }
+
+ /**
+ * Build the SAML protocol message Issuer element.
+ *
+ * @param selfEntityID the entity ID of the protocol message issuer (this entity)
+ *
+ * @return the Issuer element
+ *
+ * @throws ResolutionException if there is a fatal error during processing
+ */
+ @Nonnull private Issuer buildIssuer(@Nonnull final String selfEntityID) throws ResolutionException {
+ final Issuer issuer = (Issuer) XMLObjectSupport.buildXMLObject(Issuer.DEFAULT_ELEMENT_NAME);
+ issuer.setValue(selfEntityID);
+ return issuer;
+ }
+
+ /**
+ * Build SAML protocol message Subject element.
+ *
+ * @param resolutionContext the current attribute resolution context
+ * @param dependencyAttributes the current set of dependency attributes
+ * @param roleDescriptor the role descriptor of the protocol message issuer (this entity)
+ *
+ * @return the subject to be used in the attribute query message
+ *
+ * @throws ResolutionException if there is a fatal error during processing
+ */
+ @Nonnull private Subject buildSubject(@Nonnull final AttributeResolutionContext resolutionContext,
+ @Nonnull final Map<String, List<IdPAttributeValue>> dependencyAttributes,
+ @Nonnull final AttributeAuthorityDescriptor roleDescriptor) throws ResolutionException {
+
+ final Subject subject = getSubjectStrategy().resolve(resolutionContext, dependencyAttributes, roleDescriptor);
+ if (subject != null) {
+ try {
+ return XMLObjectSupport.cloneXMLObject(subject);
+ } catch (MarshallingException | UnmarshallingException e) {
+ throw new ResolutionException("Error cloning Subject", e);
+ }
+ }
+ throw new ResolutionException("Unable to resolve self entityID");
+ }
+
+ /**
+ * Build the list of requested attributes for the SAML protocol message.
+ *
+ * @param resolutionContext the current attribute resolution context
+ * @param dependencyAttributes the current set of dependency attributes
+ * @param roleDescriptor the role descriptor of the attribute authority being processed
+ *
+ * @return the list of requested attributes to be used in the attribute query message
+ *
+ * @throws ResolutionException if there is a fatal error during processing
+ */
+ @Nonnull private List <Attribute> buildRequestAttributes(@Nonnull final AttributeResolutionContext resolutionContext,
+ @Nonnull final Map<String, List<IdPAttributeValue>> dependencyAttributes,
+ @Nonnull final AttributeAuthorityDescriptor roleDescriptor) throws ResolutionException {
+
+ final List<Attribute> attributes = new ArrayList<>();
+
+ for (final Attribute attr : getRequestedAttributes()) {
+ assert attr != null;
+ try {
+ attributes.add(XMLObjectSupport.cloneXMLObject(attr));
+ } catch (MarshallingException | UnmarshallingException e) {
+ throw new ResolutionException("Error cloning requested Attribute", e);
+ }
+ }
+
+ return attributes;
+ }
+
+}
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/SAMLDataConnector.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/SAMLDataConnector.java
new file mode 100644
index 000000000..0f4286c4f
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/SAMLDataConnector.java
@@ -0,0 +1,253 @@
+/*
+ * 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.attribute.resolver.dc.saml.impl;
+
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.saml.common.SAMLException;
+import org.opensaml.saml.saml2.core.Response;
+import org.opensaml.saml.saml2.encryption.Decrypter;
+import org.opensaml.security.SecurityException;
+import org.opensaml.soap.client.SOAPClient;
+import org.opensaml.soap.common.SOAPException;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.resolver.DataConnector;
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.idp.attribute.resolver.dc.impl.AbstractSearchDataConnector;
+import net.shibboleth.idp.attribute.resolver.dc.saml.ExecutableQuery;
+import net.shibboleth.idp.attribute.resolver.dc.saml.ResponseData;
+import net.shibboleth.idp.attribute.resolver.dc.saml.ResponseMappingStrategy;
+import net.shibboleth.idp.attribute.resolver.dc.saml.util.impl.AssertionValidationProcessor;
+import net.shibboleth.idp.attribute.resolver.dc.saml.util.impl.DecryptionProcessor;
+import net.shibboleth.idp.attribute.resolver.dc.saml.util.impl.SubjectMatchProcessor;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * A {@link DataConnector} implementation which queries a SAML attribute authority for attribute data.
+ */
+public class SAMLDataConnector extends AbstractSearchDataConnector<ExecutableQuery, ResponseMappingStrategy> {
+
+ /** Logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(SAMLDataConnector.class);
+
+ /** SOAP client. */
+ @NonnullAfterInit private SOAPClient soapClient;
+
+ /** Decryption processor. */
+ @NonnullAfterInit private DecryptionProcessor decryptionProcessor;
+
+ /** Assertion validation processor. */
+ @NonnullAfterInit private AssertionValidationProcessor assertionValidationProcessor;
+
+ /** Assertion validation processor. */
+ @NonnullAfterInit private SubjectMatchProcessor subjectMatchProcessor;
+
+ /** Flag indicating whether to perform matching of Assertion subjects against the value in the query. */
+ private boolean subjectMatch = false;
+
+ /**
+ * Get the SOAP client instance.
+ *
+ * @return the SOAP client
+ */
+ @NonnullAfterInit public SOAPClient getSOAPClient() {
+ return soapClient;
+ }
+
+ /**
+ * Set the SOAP client instance.
+ *
+ * @param client the SOAP client
+ */
+ public void setSOAPClient(@Nullable final SOAPClient client) {
+ checkSetterPreconditions();
+ soapClient = client;
+ }
+
+ /**
+ * Get the decryption processor.
+ *
+ * @return the decryption processor
+ */
+ @NonnullAfterInit public DecryptionProcessor getDecryptionProcessor() {
+ return decryptionProcessor;
+ }
+
+ /**
+ * Set the decryption processor
+ *
+ * @param processor the Decryption processor
+ */
+ public void setDecryptionProcessor(@Nullable final DecryptionProcessor processor) {
+ checkSetterPreconditions();
+ decryptionProcessor = processor;
+ }
+
+ /**
+ * Get the Assertion validation processor.
+ *
+ * @return the Assertion validation processor
+ */
+ @NonnullAfterInit public AssertionValidationProcessor getAssertionValidationProcessor() {
+ return assertionValidationProcessor;
+ }
+
+ /**
+ * Set the Assertion validation processor
+ *
+ * @param processor the Assertion validation processor
+ */
+ public void setAssertionValidationProcessor(@Nullable final AssertionValidationProcessor processor) {
+ checkSetterPreconditions();
+ assertionValidationProcessor = processor;
+ }
+
+ /**
+ * Get the Subject match processor.
+ *
+ * @return the Subject match processor
+ */
+ @NonnullAfterInit public SubjectMatchProcessor getSubjectMatchProcessor() {
+ return subjectMatchProcessor;
+ }
+
+ /**
+ * Set the Subject match processor
+ *
+ * @param processor the Subjet match processor
+ */
+ public void setSubjectMatchProcessor(@Nullable final SubjectMatchProcessor processor) {
+ checkSetterPreconditions();
+ subjectMatchProcessor = processor;
+ }
+
+ /**
+ * Get the flag indicating whether to perform matching of Assertion subjects against the value in the query.
+ *
+ * <p>
+ * Default is <code>false</code>.
+ * </p>
+ *
+ * @return returns the flag value
+ */
+ public boolean isSubjectMatch() {
+ return subjectMatch;
+ }
+
+ /**
+ * Set the flag indicating whether to perform matching of Assertion subjects against the value in the query.
+ *
+ * <p>
+ * Default is <code>false</code>.
+ * </p>
+ *
+ * @param flag the flag value
+ */
+ public void setSubjectMatch(boolean flag) {
+ checkSetterPreconditions();
+ subjectMatch = flag;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (getSOAPClient() == null) {
+ throw new ComponentInitializationException("SOAPClient was null");
+ }
+ if (getDecryptionProcessor() == null) {
+ throw new ComponentInitializationException("DecryptionProcessor was null");
+ }
+ if (getAssertionValidationProcessor() == null) {
+ throw new ComponentInitializationException("AssertionValidationProcessor was null");
+ }
+ if (getSubjectMatchProcessor() == null) {
+ throw new ComponentInitializationException("SubjectMatchProcessor was null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nullable
+ protected Map<String, IdPAttribute> retrieveAttributes(@Nonnull final ExecutableQuery executable) throws ResolutionException {
+ try {
+ final ResponseMappingStrategy strategy = getMappingStrategy();
+ final SOAPClient localClient = getSOAPClient();
+ assert strategy != null && localClient != null;
+
+ final ResponseData responseData = executable.execute(localClient);
+
+ processResponse(responseData);
+
+ final Map<String,IdPAttribute> attributes = strategy.map(responseData);
+
+ return attributes;
+ } catch (final SAMLException | SOAPException | SecurityException e) {
+ throw new ResolutionException(getLogPrefix() + " SAML AttributeQuery failed", e);
+ }
+ }
+
+ /**
+ * Process the SAML response message.
+ *
+ * @param responseData the response data
+ *
+ * @throws ResolutionException if there is a fatal error during processing
+ */
+ protected void processResponse(@Nonnull final ResponseData responseData) throws ResolutionException {
+ final Response response = responseData.getResponse();
+
+ // Encryption on an AttributeQuery response probably isn't that common, so be efficient.
+ // Goal is to only build the Decrypter if/when needed, and only do it once
+ // for both Assertions and Assertion content.
+ DecryptionProcessor decryptionProcessor = getDecryptionProcessor();
+ Decrypter decrypter = null;
+ if (decryptionProcessor.haveEncryptedAssertions(response)) {
+ decrypter = decryptionProcessor.buildDecrypter(responseData);
+ decryptionProcessor.decryptAssertions(response, decrypter);
+ }
+
+ getAssertionValidationProcessor().validateAssertions(response, responseData.getSOAPClientContext());
+ if (response.getAssertions().isEmpty()) {
+ log.warn("{} SAML Response contained no valid Assertions", getLogPrefix());
+ throw new ResolutionException("SAML Response contained no valid Assertions");
+ }
+
+ if (decryptionProcessor.haveEncryptedContent(response)) {
+ if (decrypter == null) {
+ decrypter = decryptionProcessor.buildDecrypter(responseData);
+ }
+ decryptionProcessor.decryptAssertionContent(response, decrypter);
+ }
+
+ if (isSubjectMatch()) {
+ getSubjectMatchProcessor().process(responseData);
+ } else {
+ log.debug("Subject match is disabled, skipping");
+ return;
+ }
+ }
+
+
+
+}
\ No newline at end of file
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/SelfEntityIDResolver.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/SelfEntityIDResolver.java
new file mode 100644
index 000000000..2163a9f8b
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/SelfEntityIDResolver.java
@@ -0,0 +1,40 @@
+/*
+ * 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.attribute.resolver.dc.saml.impl;
+
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.saml.saml2.metadata.AttributeAuthorityDescriptor;
+
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext;
+
+/**
+ * Interface for a component which resolves the self entityID to use when executing
+ * an attribute query.
+ */
+public interface SelfEntityIDResolver {
+
+ @Nullable
+ String resolve(@Nonnull final AttributeResolutionContext resolutionContext,
+ @Nonnull final Map<String, List<IdPAttributeValue>> dependencyAttributes,
+ @Nonnull final AttributeAuthorityDescriptor roleDescriptor) throws ResolutionException;
+
+}
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/SimpleAggregationSAMLDataConnector.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/SimpleAggregationSAMLDataConnector.java
new file mode 100644
index 000000000..d44a9e768
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/SimpleAggregationSAMLDataConnector.java
@@ -0,0 +1,203 @@
+/*
+ * 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.attribute.resolver.dc.saml.impl;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.IdPAttributeSupport;
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.StringAttributeValue;
+import net.shibboleth.idp.attribute.resolver.AbstractDataConnector;
+import net.shibboleth.idp.attribute.resolver.DataConnector;
+import net.shibboleth.idp.attribute.resolver.PluginDependencySupport;
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext;
+import net.shibboleth.idp.attribute.resolver.context.AttributeResolverWorkContext;
+import net.shibboleth.idp.attribute.resolver.dc.saml.util.impl.AttributeAuthorityEntityIDContext;
+import net.shibboleth.idp.attribute.resolver.dc.saml.util.impl.AttributeAuthorityEntityIDReference;
+import net.shibboleth.idp.attribute.resolver.dc.saml.util.impl.AttributeAuthorityEntityIDSource;
+import net.shibboleth.idp.attribute.resolver.dc.saml.util.impl.AttributeAuthorityEntityIDValue;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * A {@link DataConnector} implementation which orchestrates calling {@link SAMLDataConnector}
+ * for each of a resolved list of attribute authority entityIDs, and aggregates the resolved attribute data.
+ */
+public class SimpleAggregationSAMLDataConnector extends AbstractDataConnector {
+
+ /** Logger. */
+ private Logger log = LoggerFactory.getLogger(SimpleAggregationSAMLDataConnector.class);
+
+ /** The SAML Data Connector which performs the actual attribute resolution operations. */
+ private SAMLDataConnector queryConnector;
+
+ /** Sources of attribute authority entityIDs. */
+ @Nonnull private List<AttributeAuthorityEntityIDSource> entityIDSources = CollectionSupport.emptyList();
+
+ /**
+ * Get the {@link SAMLDataConnector} instance to use.
+ *
+ * @return the data connector instance
+ */
+ public SAMLDataConnector getQueryConnector() {
+ return queryConnector;
+ }
+
+ /**
+ * Set the {@link SAMLDataConnector} instance to use.
+ *
+ * @param connector the data connector instance
+ */
+ public void setQueryConnector(SAMLDataConnector connector) {
+ checkSetterPreconditions();
+ queryConnector = connector;
+ }
+
+ /**
+ * Get the attribute authority entityID sources.
+ *
+ * @return the entityID sources
+ */
+ @Nonnull @NotLive @Unmodifiable public List<AttributeAuthorityEntityIDSource> getEntityIDSources() {
+ return entityIDSources;
+ }
+
+ /**
+ * Set the attribute authority entityID sources.
+ *
+ * @param sources the attribute authority entityID sources
+ */
+ public void setEntityIDSources(@Nullable final List<AttributeAuthorityEntityIDSource> sources) {
+ checkComponentActive();
+
+ if (sources == null) {
+ entityIDSources = CollectionSupport.emptyList();
+ } else {
+ entityIDSources = sources.stream()
+ .filter(Objects::nonNull)
+ .collect(CollectionSupport.nonnullCollector(Collectors.toUnmodifiableList())).get();
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nullable
+ protected Map<String, IdPAttribute> doDataConnectorResolve(@Nonnull final AttributeResolutionContext resolutionContext,
+ @Nonnull final AttributeResolverWorkContext workContext) throws ResolutionException {
+ checkComponentActive();
+
+ final List<IdPAttribute> results = new LinkedList<>();
+
+ final List<String> authtorityEntityIDs = resolveAuthorityEntityIDs(resolutionContext, workContext);
+ log.trace("{} Resolved {} attribute authority entityIDs: {}",
+ getLogPrefix(), authtorityEntityIDs.size(), authtorityEntityIDs);
+
+ for (final String authorityEntityID : authtorityEntityIDs) {
+ assert authorityEntityID != null;
+ try {
+ log.debug("Executing SAML attribute query against authority entityID: {}", authorityEntityID);
+ resolutionContext.addSubcontext(new AttributeAuthorityEntityIDContext(authorityEntityID), true);
+ final Map<String,IdPAttribute> authorityResults = getQueryConnector().resolve(resolutionContext);
+ if (authorityResults != null) {
+ log.debug("{} Authority returned {} unique attributes",
+ getLogPrefix(), authorityResults.keySet().size());
+ results.addAll(authorityResults.values());
+ } else {
+ log.debug("{} Authority returned no attributes", getLogPrefix());
+ }
+ } catch (final Exception e) {
+ log.warn("{} Fatal error executing SAML 2 attribute query against authority: {}",
+ getLogPrefix(), authorityEntityID);
+ } finally {
+ resolutionContext.removeSubcontext(AttributeAuthorityEntityIDContext.class);
+ }
+ }
+
+ final Map<String,IdPAttribute> mergedResults = IdPAttributeSupport.toMapMergeDuplicates(results);
+
+ if (log.isTraceEnabled()) {
+ log.trace("{} Decoded {} total unique aggregated attribute IDs from all attribute authorities:",
+ getLogPrefix(), mergedResults.keySet().size());
+ for (final String attributeID : mergedResults.keySet().stream().sorted().toList()) {
+ log.trace("\tAttribute ID '{}', # of values: {}",
+ attributeID, mergedResults.get(attributeID).getValues().size());
+ }
+ }
+
+ return mergedResults;
+ }
+
+ /**
+ * Resolve the attribute authority entityIDs to against which to query.
+ *
+ * @param resolutionContext current resolution context
+ * @param workContext current resolver work context
+ *
+ * @return the effective list of entityIDs to query
+ */
+ @Nonnull
+ protected List<String> resolveAuthorityEntityIDs(@Nonnull final AttributeResolutionContext resolutionContext,
+ @Nonnull final AttributeResolverWorkContext workContext) {
+
+ final List<String> entityIDs = new ArrayList<>();
+
+ // Don't resolve these unless/until we actually need them below
+ Map<String, List<IdPAttributeValue>> dependencyAttributes = null;
+
+ for (AttributeAuthorityEntityIDSource source : getEntityIDSources()) {
+ if (source instanceof AttributeAuthorityEntityIDValue) {
+ entityIDs.add(source.getValue());
+ } else if (source instanceof AttributeAuthorityEntityIDReference) {
+ final String reference = source.getValue();
+
+ if (dependencyAttributes == null) {
+ dependencyAttributes = PluginDependencySupport.getAllAttributeValues(workContext,
+ getAttributeDependencies(), getDataConnectorDependencies());
+ }
+
+ if (dependencyAttributes.containsKey(reference)) {
+ // We currently only support StringAttributeValue types here
+ entityIDs.addAll(dependencyAttributes.get(reference).stream()
+ .filter(StringAttributeValue.class::isInstance)
+ .map(StringAttributeValue.class::cast)
+ .map(StringAttributeValue::getValue)
+ .toList());
+ }
+ } else {
+ log.warn("{} Saw AttributeAuthorityEntityIDSource of unsupported type, could not process: {}",
+ getLogPrefix(), source.getClass().getName());
+ }
+ }
+
+ return entityIDs.stream()
+ .distinct()
+ .collect(CollectionSupport.nonnullCollector(Collectors.toUnmodifiableList())).get();
+ }
+
+}
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/SubjectResolver.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/SubjectResolver.java
new file mode 100644
index 000000000..2a654e05b
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/SubjectResolver.java
@@ -0,0 +1,41 @@
+/*
+ * 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.attribute.resolver.dc.saml.impl;
+
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.saml.saml2.core.Subject;
+import org.opensaml.saml.saml2.metadata.AttributeAuthorityDescriptor;
+
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext;
+
+/**
+ * Interface for a component which resolves the {@link Subject} to use when executing
+ * an attribute query.
+ */
+public interface SubjectResolver {
+
+ @Nullable
+ Subject resolve(@Nonnull final AttributeResolutionContext resolutionContext,
+ @Nonnull final Map<String, List<IdPAttributeValue>> dependencyAttributes,
+ @Nonnull final AttributeAuthorityDescriptor roleDescriptor) throws ResolutionException;
+
+}
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/package-info.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/package-info.java
new file mode 100644
index 000000000..231ae28e8
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/impl/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+/**
+ * A {@link net.shibboleth.idp.attribute.resolver.DataConnector} implementation that
+ * queries data from a SAML attribute authority.
+ */
+ at NonnullElements
+package net.shibboleth.idp.attribute.resolver.dc.saml.impl;
+
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/ChainingAuthorityEntityIDResolver.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/ChainingAuthorityEntityIDResolver.java
new file mode 100644
index 000000000..16bfa0e83
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/ChainingAuthorityEntityIDResolver.java
@@ -0,0 +1,106 @@
+/*
+ * 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.attribute.resolver.dc.saml.plugin.impl;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext;
+import net.shibboleth.idp.attribute.resolver.dc.saml.impl.AttributeAuthorityEntityIDResolver;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.AbstractInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * An implementation of {@link AttributeAuthorityEntityIDResolver} which implements a chain-of-responsibility pattern
+ * using a list of configured resolvers.
+ */
+public class ChainingAuthorityEntityIDResolver extends AbstractInitializableComponent
+ implements AttributeAuthorityEntityIDResolver {
+
+ /** Logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(ChainingAuthorityEntityIDResolver.class);
+
+ /** List of chain members. */
+ @NonnullAfterInit private List<AttributeAuthorityEntityIDResolver> resolvers;
+
+ /**
+ * Get the list of chain members.
+ *
+ * @return the list of chain members
+ */
+ @NotLive
+ @Unmodifiable
+ @NonnullAfterInit public List<AttributeAuthorityEntityIDResolver> getResolvers() {
+ return resolvers;
+ }
+
+ /**
+ * Set the list of chain members.
+ *
+ * @param chain the chain of members
+ */
+ public void setResolvers(@Nullable final List<AttributeAuthorityEntityIDResolver> chain) {
+ checkSetterPreconditions();
+ if (chain == null) {
+ resolvers = CollectionSupport.emptyList();
+ } else {
+ resolvers = chain.stream()
+ .filter(Objects::nonNull)
+ .collect(CollectionSupport.nonnullCollector(Collectors.toUnmodifiableList())).get();
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (getResolvers() == null) {
+ throw new ComponentInitializationException("List chain member resolvers was null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nullable
+ public String resolve(@Nonnull final AttributeResolutionContext resolutionContext,
+ @Nonnull final Map<String, List<IdPAttributeValue>> dependencyAttributes) throws ResolutionException {
+ checkComponentActive();
+
+ for (final AttributeAuthorityEntityIDResolver resolver : getResolvers()) {
+ final String entityID = resolver.resolve(resolutionContext, dependencyAttributes);
+ if (entityID != null) {
+ log.debug("Resolved entityID '{}' using resolver impl: {}", entityID, resolver.getClass().getName());
+ return entityID;
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/ChainingSelfEntityIDResolver.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/ChainingSelfEntityIDResolver.java
new file mode 100644
index 000000000..788135245
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/ChainingSelfEntityIDResolver.java
@@ -0,0 +1,108 @@
+/*
+ * 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.attribute.resolver.dc.saml.plugin.impl;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.saml.saml2.metadata.AttributeAuthorityDescriptor;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext;
+import net.shibboleth.idp.attribute.resolver.dc.saml.impl.SelfEntityIDResolver;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.AbstractInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * An implementation of {@link SelfEntityIDResolver} which implements a chain-of-responsibility pattern
+ * using a list of configured resolvers.
+ */
+public class ChainingSelfEntityIDResolver extends AbstractInitializableComponent
+ implements SelfEntityIDResolver {
+
+ /** Logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(ChainingSelfEntityIDResolver.class);
+
+ /** List of chain members. */
+ @NonnullAfterInit private List<SelfEntityIDResolver> resolvers;
+
+ /**
+ * Get the list of chain members.
+ *
+ * @return the list of chain members
+ */
+ @NotLive
+ @Unmodifiable
+ @NonnullAfterInit public List<SelfEntityIDResolver> getResolvers() {
+ return resolvers;
+ }
+
+ /**
+ * Set the list of chain members.
+ *
+ * @param chain the chain of members
+ */
+ public void setResolvers(@Nullable final List<SelfEntityIDResolver> chain) {
+ checkSetterPreconditions();
+ if (chain == null) {
+ resolvers = CollectionSupport.emptyList();
+ } else {
+ resolvers = chain.stream()
+ .filter(Objects::nonNull)
+ .collect(CollectionSupport.nonnullCollector(Collectors.toUnmodifiableList())).get();
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (getResolvers() == null) {
+ throw new ComponentInitializationException("List chain member resolvers was null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nullable
+ public String resolve(@Nonnull final AttributeResolutionContext resolutionContext,
+ @Nonnull final Map<String, List<IdPAttributeValue>> dependencyAttributes,
+ @Nonnull final AttributeAuthorityDescriptor roleDescriptor) throws ResolutionException {
+ checkComponentActive();
+
+ for (final SelfEntityIDResolver resolver : getResolvers()) {
+ final String entityID = resolver.resolve(resolutionContext, dependencyAttributes, roleDescriptor);
+ if (entityID != null) {
+ log.debug("Resolved entityID '{}' using resolver impl: {}", entityID, resolver.getClass().getName());
+ return entityID;
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/ChainingSubjectResolver.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/ChainingSubjectResolver.java
new file mode 100644
index 000000000..7245a34f1
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/ChainingSubjectResolver.java
@@ -0,0 +1,108 @@
+/*
+ * 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.attribute.resolver.dc.saml.plugin.impl;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.saml.saml2.core.Subject;
+import org.opensaml.saml.saml2.metadata.AttributeAuthorityDescriptor;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext;
+import net.shibboleth.idp.attribute.resolver.dc.saml.impl.SubjectResolver;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.AbstractInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * An implementation of {@link SubjectResolver} which implements a chain-of-responsibility pattern
+ * using a list of configured resolvers.
+ */
+public class ChainingSubjectResolver extends AbstractInitializableComponent implements SubjectResolver {
+
+ /** Logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(ChainingSubjectResolver.class);
+
+ /** List of chain members. */
+ @NonnullAfterInit private List<SubjectResolver> resolvers;
+
+ /**
+ * Get the list of chain members.
+ *
+ * @return the list of chain members
+ */
+ @NotLive
+ @Unmodifiable
+ @NonnullAfterInit public List<SubjectResolver> getResolvers() {
+ return resolvers;
+ }
+
+ /**
+ * Set the list of chain members.
+ *
+ * @param chain the chain of members
+ */
+ public void setResolvers(@Nullable final List<SubjectResolver> chain) {
+ checkSetterPreconditions();
+ if (chain == null) {
+ resolvers = CollectionSupport.emptyList();
+ } else {
+ resolvers = chain.stream()
+ .filter(Objects::nonNull)
+ .collect(CollectionSupport.nonnullCollector(Collectors.toUnmodifiableList())).get();
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (getResolvers() == null) {
+ throw new ComponentInitializationException("List chain member resolvers was null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nullable
+ public Subject resolve(@Nonnull final AttributeResolutionContext resolutionContext,
+ @Nonnull final Map<String, List<IdPAttributeValue>> dependencyAttributes,
+ @Nonnull final AttributeAuthorityDescriptor roleDescriptor) throws ResolutionException {
+ checkComponentActive();
+
+ for (final SubjectResolver resolver : getResolvers()) {
+ final Subject subject = resolver.resolve(resolutionContext, dependencyAttributes, roleDescriptor);
+ if (subject != null) {
+ log.debug("Resolved Subject using resolver impl: {}", resolver.getClass().getName());
+ return subject;
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/ContextAuthorityEntityIDResolver.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/ContextAuthorityEntityIDResolver.java
new file mode 100644
index 000000000..9d9cadcd4
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/ContextAuthorityEntityIDResolver.java
@@ -0,0 +1,50 @@
+/*
+ * 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.attribute.resolver.dc.saml.plugin.impl;
+
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext;
+import net.shibboleth.idp.attribute.resolver.dc.saml.impl.AttributeAuthorityEntityIDResolver;
+import net.shibboleth.idp.attribute.resolver.dc.saml.util.impl.AttributeAuthorityEntityIDContext;
+
+/**
+ * An implementation of {@link AttributeAuthorityEntityIDResolver} which resolves
+ * from the {@link AttributeAuthorityEntityIDContext} child of the input {@link AttributeResolutionContext}
+ * if present.
+ */
+public class ContextAuthorityEntityIDResolver implements AttributeAuthorityEntityIDResolver {
+
+ /** {@inheritDoc} */
+ @Override
+ @Nullable
+ public String resolve(@Nonnull final AttributeResolutionContext resolutionContext,
+ @Nonnull Map<String, List<IdPAttributeValue>> dependencyAttributes) throws ResolutionException {
+
+ final AttributeAuthorityEntityIDContext context =
+ resolutionContext.getSubcontext(AttributeAuthorityEntityIDContext.class);
+ if (context != null) {
+ return context.getAuthorityEntityID();
+ }
+ return null;
+ }
+
+}
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/CriteriaDecryptionConfigurationLookup.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/CriteriaDecryptionConfigurationLookup.java
new file mode 100644
index 000000000..90c4e01a5
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/CriteriaDecryptionConfigurationLookup.java
@@ -0,0 +1,124 @@
+/*
+ * 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.attribute.resolver.dc.saml.plugin.impl;
+
+import java.util.List;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.criterion.ProfileIDCriterion;
+import org.opensaml.saml.criterion.RoleDescriptorCriterion;
+import org.opensaml.xmlsec.DecryptionConfiguration;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.attribute.resolver.dc.saml.ResponseData;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.AbstractInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.Resolver;
+import net.shibboleth.shared.resolver.ResolverException;
+
+/**
+ * A component which resolves a list of {@link DecryptionConfiguration} using a delegated criteria-based resolver.
+ */
+public class CriteriaDecryptionConfigurationLookup extends AbstractInitializableComponent
+ implements Function<ResponseData,List<DecryptionConfiguration>> {
+
+ /** Logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(ChainingSelfEntityIDResolver.class);
+
+ /** Resolver for decryption configurations. */
+ @NonnullAfterInit private Resolver<List<DecryptionConfiguration>, CriteriaSet> configurationResolver;
+
+ /** Security configuration profile ID. */
+ @NonnullAfterInit private String securityConfigurationProfileId;
+
+ /**
+ * Get the resolver delegate.
+ *
+ * @return the resolver
+ */
+ @NonnullAfterInit public Resolver<List<DecryptionConfiguration>,CriteriaSet> getConfigurationResolver() {
+ return configurationResolver;
+ }
+
+ /**
+ * Set the resolver delegate.
+ *
+ * @param resolver the resolver instance
+ */
+ public void setConfigurationResolver(@Nullable final Resolver<List<DecryptionConfiguration>, CriteriaSet> resolver) {
+ checkSetterPreconditions();
+ configurationResolver = resolver;
+ }
+
+ /**
+ * Set the security configuration profile ID to use.
+ *
+ * @param profileId the profile ID, or null
+ */
+ public void setSecurityConfigurationProfileId(@Nullable final String profileId) {
+ checkSetterPreconditions();
+ securityConfigurationProfileId = StringSupport.trimOrNull(profileId);
+ }
+
+ /** {@inheritDoc} */
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (configurationResolver == null) {
+ throw new ComponentInitializationException("DecryptionConfiguration resolver cannot be null");
+ }
+ if (securityConfigurationProfileId == null) {
+ throw new ComponentInitializationException("Security config profile ID cannot be null");
+ }
+
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nullable
+ public List<DecryptionConfiguration> apply(@Nullable final ResponseData responseData) {
+ checkComponentActive();
+
+ // Implementing java.util.Function won't allow for this arg to be @Nonnull
+ if (responseData == null) {
+ return CollectionSupport.emptyList();
+ }
+
+ final CriteriaSet criteria = new CriteriaSet(
+ new RoleDescriptorCriterion(responseData.getAttributeAuthorityRoleDescriptor()),
+ new ProfileIDCriterion(securityConfigurationProfileId));
+
+ try {
+ final List<DecryptionConfiguration> configs = getConfigurationResolver().resolveSingle(criteria);
+ if (configs == null) {
+ log.warn("Failed to resolve list of DecryptionConfiguration from criteria");
+ return null;
+ }
+ return configs;
+ } catch (final ResolverException e) {
+ log.warn("Fatal error resolving DecryptionConfiguration via criteria");
+ return null;
+ }
+ }
+
+}
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/CriteriaSelfEntityIDResolver.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/CriteriaSelfEntityIDResolver.java
new file mode 100644
index 000000000..1085d1a6c
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/CriteriaSelfEntityIDResolver.java
@@ -0,0 +1,101 @@
+/*
+ * 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.attribute.resolver.dc.saml.plugin.impl;
+
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.saml.criterion.RoleDescriptorCriterion;
+import org.opensaml.saml.saml2.metadata.AttributeAuthorityDescriptor;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext;
+import net.shibboleth.idp.attribute.resolver.dc.saml.impl.SelfEntityIDResolver;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.AbstractInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.Resolver;
+import net.shibboleth.shared.resolver.ResolverException;
+
+/**
+ * An implementation of {@link SelfEntityIDResolver} which uses a delegated resolver
+ * based on an input {@link CriteriaSet}.
+ */
+public class CriteriaSelfEntityIDResolver extends AbstractInitializableComponent implements SelfEntityIDResolver {
+
+ /** Logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(ChainingSelfEntityIDResolver.class);
+
+ /** Resolver for the self entityID, based on the peer entity data. */
+ @NonnullAfterInit private Resolver<String, CriteriaSet> selfEntityIDResolver;
+
+ /**
+ * Get the resolver for the self entityID.
+ *
+ * @return the resolver
+ */
+ @NonnullAfterInit public Resolver<String,CriteriaSet> getSelfEntityIDResolver() {
+ return selfEntityIDResolver;
+ }
+
+ /**
+ * Set the resolver for the self entityID.
+ *
+ * @param resolver the resolver instance
+ */
+ public void setSelfEntityIDResolver(@Nullable final Resolver<String, CriteriaSet> resolver) {
+ checkSetterPreconditions();
+ selfEntityIDResolver = resolver;
+ }
+
+ /** {@inheritDoc} */
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (selfEntityIDResolver == null) {
+ throw new ComponentInitializationException("Self entityID resolver cannot be null");
+ }
+
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nullable
+ public String resolve(@Nonnull final AttributeResolutionContext resolutionContext,
+ @Nonnull final Map<String, List<IdPAttributeValue>> dependencyAttributes,
+ @Nonnull final AttributeAuthorityDescriptor roleDescriptor) throws ResolutionException {
+ checkComponentActive();
+
+ final CriteriaSet criteria = new CriteriaSet(new RoleDescriptorCriterion(roleDescriptor));
+ try {
+ final String selfEntityID = getSelfEntityIDResolver().resolveSingle(criteria);
+ if (selfEntityID == null) {
+ throw new ResolutionException("Unable to resolve self entityID from peer RoleDescriptor");
+ }
+ log.debug("Resolved self entityID via criteria: {}", selfEntityID);
+ return selfEntityID;
+ } catch (final ResolverException e) {
+ throw new ResolutionException("Fatal error resolving self entityID from peer RoleDescriptor", e);
+ }
+ }
+
+}
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/DependencyAttributeSubjectResolver.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/DependencyAttributeSubjectResolver.java
new file mode 100644
index 000000000..f9fbd324b
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/DependencyAttributeSubjectResolver.java
@@ -0,0 +1,249 @@
+/*
+ * 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.attribute.resolver.dc.saml.plugin.impl;
+
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+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.saml2.core.NameID;
+import org.opensaml.saml.saml2.core.Subject;
+import org.opensaml.saml.saml2.metadata.AttributeAuthorityDescriptor;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.ScopedStringAttributeValue;
+import net.shibboleth.idp.attribute.StringAttributeValue;
+import net.shibboleth.idp.attribute.XMLObjectAttributeValue;
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext;
+import net.shibboleth.idp.attribute.resolver.dc.saml.impl.SubjectResolver;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.AbstractInitializableComponent;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+import net.shibboleth.shared.xml.SerializeSupport;
+
+/**
+ * An implementation of {@link SubjectResolver} which returns a {@link Subject} using a {@link NameID} built
+ * using the value from one of an ordered list of dependency attributes.
+ *
+ * <p>
+ * Dependency attributes are searched in the order specified and the first one found to have a value
+ * of a supported type will be used. Supported types are:
+ * </p>
+ * <ul>
+ * <li>{@link StringAttributeValue}</li>
+ * <li>{@link ScopedStringAttributeValue}</li>
+ * <li>{@link XMLObjectAttributeValue} containing a {@link NameID}</li>
+ * </ul>
+ */
+public class DependencyAttributeSubjectResolver extends AbstractInitializableComponent implements SubjectResolver {
+
+ /** Logger. */
+ @Nonnull final Logger log = LoggerFactory.getLogger(DependencyAttributeSubjectResolver.class);
+
+ /** List of dependency attribute IDs to search for a value. */
+ @Nonnull private List<String> attributeIDs = CollectionSupport.emptyList();
+
+ /** Map allowing to customize the NameID format URI on a per-attributeID basis. */
+ @Nonnull private Map<String,String> nameIDFormatMap = CollectionSupport.emptyMap();
+
+ /** The default format URI used to construct the NameID if a custom format for the attribute ID is
+ * not defined in {@link #getNameIDFormatMap()}. */
+ @Nullable private String defaultNameIDFormat;
+
+ /**
+ * Get the list of dependency attribute IDs to search for a value.
+ *
+ * @return the list of attribute IDs, may be empty
+ */
+ @Unmodifiable
+ @NotLive
+ @Nonnull public List<String> getAttributeIDs() {
+ return attributeIDs;
+ }
+
+ /**
+ * Set the list of dependency attribute IDs to search for a value.
+ *
+ * @param values the list of attribute IDs
+ */
+ public void setAttributeIDs(@Nullable final List<String> values) {
+ checkSetterPreconditions();
+ if (values == null) {
+ attributeIDs = CollectionSupport.emptyList();
+ } else {
+ attributeIDs = CollectionSupport.copyToList(StringSupport.normalizeStringCollection(values));
+ }
+ }
+
+ /**
+ * Get the map allowing to customize the NameID format URI on a per-attributeID basis.
+ *
+ * @return the custom format map
+ */
+ @Unmodifiable
+ @NotLive
+ @Nonnull public Map<String,String> getNameIDFormatMap() {
+ return nameIDFormatMap;
+ }
+
+ /**
+ * Set the map allowing to customize the NameID format URI on a per-attributeID basis.
+ *
+ * @param formatMap the custom format map
+ */
+ public void setNameIDFormatMap(@Nullable final Map<String,String> formatMap) {
+ checkSetterPreconditions();
+ if (formatMap == null) {
+ nameIDFormatMap = CollectionSupport.emptyMap();
+ } else {
+ nameIDFormatMap = CollectionSupport.copyToMap(formatMap);
+ }
+ }
+
+ /**
+ * Get the default format URI used to construct the NameID if a custom format for the attribute ID is
+ * not defined in {@link #getNameIDFormatMap()}.
+ *
+ * @return the format URI
+ */
+ @Nullable public String getDefaultNameIDFormat() {
+ return defaultNameIDFormat;
+ }
+
+ /**
+ * Set the default format URI used to construct the NameID if a custom format for the attribute ID is
+ * not defined in {@link #getNameIDFormatMap()}.
+ *
+ * @param uri the format URI
+ */
+ public void setDefaultNameIDFormat(@Nullable final String uri) {
+ checkSetterPreconditions();
+ defaultNameIDFormat = StringSupport.trimOrNull(uri);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nullable
+ public Subject resolve(@Nonnull final AttributeResolutionContext resolutionContext,
+ @Nonnull final Map<String, List<IdPAttributeValue>> dependencyAttributes,
+ @Nonnull final AttributeAuthorityDescriptor roleDescriptor) throws ResolutionException {
+
+ checkComponentActive();
+
+ for (final String attributeID : getAttributeIDs()) {
+ assert attributeID != null;
+ log.trace("Evaluating attribute ID: {}", attributeID);
+ if (dependencyAttributes.containsKey(attributeID)) {
+ log.trace("Dependency attribute '{}' present, checking values");
+ for (final IdPAttributeValue value : dependencyAttributes.get(attributeID)) {
+ if (value instanceof StringAttributeValue stringValue) {
+ log.debug("In dependency attribute '{}' found string value '{}'",
+ attributeID, stringValue.getValue());
+ return buildSubject(stringValue.getValue(), attributeID);
+ } else if (value instanceof ScopedStringAttributeValue scopedStringValue) {
+ final String scopedString = scopedStringValue.getValue() + "@" + scopedStringValue.getScope();
+ log.debug("In dependency attribute '{}' found scoped string value '{}'",
+ attributeID, scopedString);
+ return buildSubject(scopedString, attributeID);
+ } else if (value instanceof XMLObjectAttributeValue xmlObjectValue
+ && xmlObjectValue.getValue() instanceof NameID nameIDValue) {
+ try {
+ if (log.isDebugEnabled()) {
+ log.debug("In dependency attribute '{}' found NameID value '{}'", attributeID,
+ SerializeSupport.nodeToString(XMLObjectSupport.marshall(nameIDValue)));
+ }
+ } catch (final MarshallingException e) {
+ log.debug("Error while marshalling+serializing NameID for logging", e);
+ }
+ final Subject subject = buildSubject(nameIDValue);
+ if (subject != null) {
+ return subject;
+ } else {
+ log.warn("Error constructing Subject from NameID value, processing remaining attributes");
+ continue;
+ }
+ }
+ }
+ }
+ }
+
+ log.debug("Could not resolve Subject based on dependency attributes");
+ return null;
+ }
+
+ /**
+ * Build a Subject using the specified string as the NameID value.
+ *
+ * @param value the string value
+ * @param attributeID the attribute ID which contained the specified value
+ *
+ * @return newly constructed Subject instance
+ */
+ @Nonnull private Subject buildSubject(@Nonnull final String value, @Nonnull final String attributeID) {
+ final NameID nameID = (NameID) XMLObjectSupport.buildXMLObject(NameID.DEFAULT_ELEMENT_NAME);
+ nameID.setValue(value);
+ nameID.setFormat(determinieNameIDFormat(attributeID));
+
+ final Subject subject = (Subject) XMLObjectSupport.buildXMLObject(Subject.DEFAULT_ELEMENT_NAME);
+ subject.setNameID(nameID);
+ return subject;
+ }
+
+ /**
+ * Build a Subject using the specified NameID.
+ *
+ * @param value the NameID value
+ *
+ * @return newly constructed Subject instance, or null if there was a fatal error cloning the input NameID
+ */
+ @Nullable private Subject buildSubject(@Nonnull final NameID value) {
+ try {
+ final NameID nameID = XMLObjectSupport.cloneXMLObject(value);
+
+ final Subject subject = (Subject) XMLObjectSupport.buildXMLObject(Subject.DEFAULT_ELEMENT_NAME);
+ subject.setNameID(nameID);
+ return subject;
+ } catch (final MarshallingException | UnmarshallingException e) {
+ log.warn("Fatal error cloning NameID for Subject construction", e);
+ return null;
+ }
+ }
+
+ /**
+ * Determine the NamedID Format to use for the given dependency attribute ID.
+ *
+ * @param attributeID the attribute ID to process
+ *
+ * @return the NameID Format to use for attributes of the specified ID
+ */
+ @Nullable private String determinieNameIDFormat(@Nonnull final String attributeID) {
+ final String mappedValue = getNameIDFormatMap().get(attributeID);
+ if (mappedValue != null) {
+ return mappedValue;
+ }
+ return getDefaultNameIDFormat();
+ }
+
+}
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/package-info.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/package-info.java
new file mode 100644
index 000000000..62a071d61
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/plugin/impl/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+/**
+ * Plug-in component interface implementations used when querying a SAML attribute authority.
+ */
+ at NonnullElements
+package net.shibboleth.idp.attribute.resolver.dc.saml.plugin.impl;
+
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/AssertionValidationProcessor.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/AssertionValidationProcessor.java
new file mode 100644
index 000000000..5934a59c2
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/AssertionValidationProcessor.java
@@ -0,0 +1,222 @@
+/*
+ * 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.attribute.resolver.dc.saml.util.impl;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.InOutOperationContext;
+import org.opensaml.saml.common.assertion.AssertionValidationException;
+import org.opensaml.saml.common.assertion.ValidationContext;
+import org.opensaml.saml.common.assertion.ValidationProcessingData;
+import org.opensaml.saml.common.assertion.ValidationResult;
+import org.opensaml.saml.saml2.assertion.SAML20AssertionValidator;
+import org.opensaml.saml.saml2.assertion.messaging.AssertionValidationInput;
+import org.opensaml.saml.saml2.assertion.messaging.AssertionValidationNetworkInformationSupplier;
+import org.opensaml.saml.saml2.assertion.messaging.BasicNetworkInformationSupplier;
+import org.opensaml.saml.saml2.core.Assertion;
+import org.opensaml.saml.saml2.core.Response;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.collection.Pair;
+import net.shibboleth.shared.component.AbstractInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.FunctionSupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * A helper component for validating the {@link Assertion} content of a {@link Response}.
+ */
+public class AssertionValidationProcessor extends AbstractInitializableComponent {
+
+ /** Logger. */
+ private Logger log = LoggerFactory.getLogger(AssertionValidationProcessor.class);
+
+ /** Function that builds a {@link ValidationContext} instance based on a
+ * {@link AssertionValidationInput} instance. */
+ @NonnullAfterInit private Function<AssertionValidationInput, ValidationContext> assertionValidationContextBuilder;
+
+ /** The SAML 2.0 Assertion validator lookup function, may be null.*/
+ @NonnullAfterInit
+ private Function<Pair<InOutOperationContext, Assertion>, SAML20AssertionValidator> assertionValidatorLookup;
+
+ /**
+ * Get the function that builds a {@link ValidationContext} instance based on a
+ * {@link AssertionValidationInput} instance.
+ *
+ * @return the builder function
+ */
+ @NonnullAfterInit
+ public Function<AssertionValidationInput, ValidationContext> getAssertionValidationContextBuilder() {
+ return assertionValidationContextBuilder;
+ }
+
+ /**
+ * Set the function that builds a {@link ValidationContext} instance based on a
+ * {@link AssertionValidationInput} instance.
+ *
+ * @param builder the builder function
+ */
+ public void setAssertionValidationContextBuilder(
+ @Nonnull final Function<AssertionValidationInput, ValidationContext> builder) {
+ checkSetterPreconditions();
+ assertionValidationContextBuilder = Constraint.isNotNull(builder,
+ "ValidationContext builder cannot be null");
+ }
+
+ /**
+ * Set the locally-configured Assertion validator.
+ *
+ * @param validator the local Assertion validator, may be null
+ */
+ public void setAssertionValidator(@Nullable final SAML20AssertionValidator validator) {
+ checkSetterPreconditions();
+ assertionValidatorLookup = FunctionSupport.constant(validator);
+ }
+
+ /**
+ * Get the Assertion validator lookup function.
+ *
+ * @return the lookup function
+ */
+ @NonnullAfterInit
+ public Function<Pair<InOutOperationContext, Assertion>, SAML20AssertionValidator> getAssertionValidatorLookup() {
+ return assertionValidatorLookup;
+ }
+
+ /**
+ * Set the Assertion validator lookup function.
+ *
+ * @param function the Assertion validator lookup function
+ */
+ public void setAssertionValidatorLookup(
+ @Nonnull final Function<Pair<InOutOperationContext, Assertion>, SAML20AssertionValidator> function) {
+ checkSetterPreconditions();
+ assertionValidatorLookup = Constraint.isNotNull(function, "AssertionValidator lookup function cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (getAssertionValidationContextBuilder() == null) {
+ throw new ComponentInitializationException("Assertion validation context builder was null");
+ }
+ if (getAssertionValidatorLookup() == null) {
+ throw new ComponentInitializationException("Assertion validator lookup function was null");
+ }
+ }
+
+ /**
+ * Validate the assertions in the specified Response
+ *
+ * <p>
+ * If an assertion fails validation, it will be removed from the response.
+ * </p>
+ *
+ * @param response the response being evaluated
+ * @param opContext the SOAP client operation context
+ *
+ * @throws ResolutionException if there was a fatal error determining assertion validity
+ */
+ public void validateAssertions(@Nonnull final Response response, @Nonnull final InOutOperationContext opContext)
+ throws ResolutionException {
+ checkComponentActive();
+
+ final Collection<Assertion> notValid = new ArrayList<>();
+
+ for (final Assertion assertion : response.getAssertions()) {
+ assert assertion != null;
+
+ final SAML20AssertionValidator validator =
+ getAssertionValidatorLookup().apply(new Pair<>(opContext, assertion));
+ if (validator == null) {
+ log.warn("No SAML20AssertionValidator was available, terminating");
+ throw new ResolutionException("No SAML20AssertionValidator was available");
+ }
+
+ try {
+ final ValidationContext validationContext = buildValidationContext(opContext, assertion);
+ final ValidationResult validationResult = validator.validate(assertion, validationContext);
+ if (validationResult != ValidationResult.VALID) {
+ notValid.add(assertion);
+ }
+ processAssertionValidationResult(validationContext, validationResult, assertion, opContext);
+ } catch (final Exception e) {
+ log.warn("There was a problem determining Assertion validity", e);
+ throw new ResolutionException("There was a problem determing SAML Assertion validity", e);
+ }
+
+ }
+
+ response.getAssertions().removeAll(notValid);
+ }
+
+ /**
+ * Build the Assertion ValidationContext.
+ *
+ * @param opContext the current operation context
+ * @param assertion the assertion which is to be validated
+ *
+ * @return the new Assertion validation context to use
+ *
+ * @throws AssertionValidationException if no validation context instance could be built
+ */
+ @Nonnull protected ValidationContext buildValidationContext(@Nonnull final InOutOperationContext opContext,
+ @Nonnull final Assertion assertion) throws AssertionValidationException {
+
+ // For this use case we don't have any of this input that is relevant, so just supply an empty impl
+ final AssertionValidationNetworkInformationSupplier networkInformation = new BasicNetworkInformationSupplier();
+
+ final ValidationContext validationContext = getAssertionValidationContextBuilder().apply(
+ new AssertionValidationInput(assertion, opContext, networkInformation));
+
+ if (validationContext == null) {
+ log.warn("ValidationContext produced was null");
+ throw new AssertionValidationException("Assertion ValidationContext was null");
+ }
+
+ return validationContext;
+ }
+
+ /**
+ * Process the result of the assertion validation.
+ *
+ * @param validationContext the Assertion validation context
+ * @param validationResult the Assertion validation result
+ * @param assertion the assertion being evaluated produced
+ * @param opContext the current profile request context
+ */
+ protected void processAssertionValidationResult(@Nonnull final ValidationContext validationContext,
+ @Nonnull final ValidationResult validationResult, @Nonnull final Assertion assertion,
+ @Nonnull final InOutOperationContext opContext) {
+
+ log.debug("Assertion validation result was: {}", validationResult);
+ if (validationResult != ValidationResult.VALID) {
+ log.info("Assertion validation failure(s): {}",validationContext.getValidationFailureMessages());
+ }
+
+ assertion.getObjectMetadata().put(new ValidationProcessingData(validationContext, validationResult));
+ }
+
+}
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/AttributeAuthorityEntityIDContext.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/AttributeAuthorityEntityIDContext.java
new file mode 100644
index 000000000..775f5db0c
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/AttributeAuthorityEntityIDContext.java
@@ -0,0 +1,53 @@
+/*
+ * 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.attribute.resolver.dc.saml.util.impl;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.BaseContext;
+
+import net.shibboleth.idp.attribute.resolver.dc.saml.impl.SimpleAggregationSAMLDataConnector;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.StringSupport;
+
+/**
+ * Used by {@link SimpleAggregationSAMLDataConnector} hold the current attribute authority entityID
+ * from which attributes are being resolved.
+ */
+public class AttributeAuthorityEntityIDContext extends BaseContext {
+
+ /** The authority entityID. */
+ private String authorityEntityID;
+
+ /**
+ * Constructor.
+ *
+ * @param entityID the authority entityID
+ */
+ public AttributeAuthorityEntityIDContext(@Nonnull final String entityID) {
+ super();
+ authorityEntityID = Constraint.isNotNull(StringSupport.trimOrNull(entityID), "Authority entityID was null");
+ }
+
+ /**
+ * Get the authority entityID.
+ *
+ * @return the entityID
+ */
+ public String getAuthorityEntityID() {
+ return authorityEntityID;
+ }
+
+}
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/AttributeAuthorityEntityIDReference.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/AttributeAuthorityEntityIDReference.java
new file mode 100644
index 000000000..3e295d683
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/AttributeAuthorityEntityIDReference.java
@@ -0,0 +1,34 @@
+/*
+ * 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.attribute.resolver.dc.saml.util.impl;
+
+import javax.annotation.Nonnull;
+
+/**
+ * Sub-type of {@link AttributeAuthorityEntityIDSource} which specifies a reference
+ * to an entityID in the form of an attribute resolver dependency attribute name.
+ */
+public class AttributeAuthorityEntityIDReference extends AttributeAuthorityEntityIDSource {
+
+ /**
+ * Constructor.
+ *
+ * @param reference the authority entityID reference (dependency attribute name)
+ */
+ protected AttributeAuthorityEntityIDReference(@Nonnull final String reference) {
+ super(reference);
+ }
+
+}
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/AttributeAuthorityEntityIDSource.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/AttributeAuthorityEntityIDSource.java
new file mode 100644
index 000000000..ec6bf16b9
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/AttributeAuthorityEntityIDSource.java
@@ -0,0 +1,49 @@
+/*
+ * 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.attribute.resolver.dc.saml.util.impl;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.StringSupport;
+
+/**
+ * Abstraction used to represent the source data for deriving an attribute authority entityID.
+ */
+public abstract class AttributeAuthorityEntityIDSource {
+
+ /** The source value. */
+ @Nonnull private String value;
+
+ /**
+ * Constructor.
+ *
+ * @param newValue the source value
+ */
+ protected AttributeAuthorityEntityIDSource(@Nonnull final String newValue) {
+ super();
+ value = Constraint.isNotNull(StringSupport.trimOrNull(newValue), "Source value was null");
+ }
+
+ /**
+ * Get the source value
+ *
+ * @return the value
+ */
+ @Nonnull public String getValue() {
+ return value;
+ }
+
+}
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/AttributeAuthorityEntityIDValue.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/AttributeAuthorityEntityIDValue.java
new file mode 100644
index 000000000..1e00f4391
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/AttributeAuthorityEntityIDValue.java
@@ -0,0 +1,34 @@
+/*
+ * 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.attribute.resolver.dc.saml.util.impl;
+
+import javax.annotation.Nonnull;
+
+/**
+ *
+ * Sub-type of {@link AttributeAuthorityEntityIDSource} which specifies the entityID directly by value.
+ */
+public class AttributeAuthorityEntityIDValue extends AttributeAuthorityEntityIDSource {
+
+ /**
+ * Constructor.
+ *
+ * @param entityID the authority entityID value
+ */
+ protected AttributeAuthorityEntityIDValue(@Nonnull final String entityID) {
+ super(entityID);
+ }
+
+}
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/DecryptionProcessor.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/DecryptionProcessor.java
new file mode 100644
index 000000000..a2473f82a
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/DecryptionProcessor.java
@@ -0,0 +1,367 @@
+/*
+ * 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.attribute.resolver.dc.saml.util.impl;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.core.xml.io.MarshallingException;
+import org.opensaml.core.xml.util.XMLObjectSupport;
+import org.opensaml.saml.common.SAMLObject;
+import org.opensaml.saml.saml2.core.Assertion;
+import org.opensaml.saml.saml2.core.Attribute;
+import org.opensaml.saml.saml2.core.AttributeStatement;
+import org.opensaml.saml.saml2.core.EncryptedAssertion;
+import org.opensaml.saml.saml2.core.EncryptedAttribute;
+import org.opensaml.saml.saml2.core.EncryptedID;
+import org.opensaml.saml.saml2.core.NameID;
+import org.opensaml.saml.saml2.core.Response;
+import org.opensaml.saml.saml2.core.Subject;
+import org.opensaml.saml.saml2.core.SubjectConfirmation;
+import org.opensaml.saml.saml2.encryption.Decrypter;
+import org.opensaml.xmlsec.DecryptionConfiguration;
+import org.opensaml.xmlsec.DecryptionParameters;
+import org.opensaml.xmlsec.DecryptionParametersResolver;
+import org.opensaml.xmlsec.criterion.DecryptionConfigurationCriterion;
+import org.opensaml.xmlsec.encryption.support.DecryptionException;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.attribute.resolver.ResolutionException;
+import net.shibboleth.idp.attribute.resolver.dc.saml.ResponseData;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.AbstractInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+
+/**
+ * A helper component for processing encrypted content of a {@link Response}.
+ */
+public class DecryptionProcessor extends AbstractInitializableComponent {
+
+ /** Logger. */
+ private Logger log = LoggerFactory.getLogger(DecryptionProcessor.class);
+
+ /** Decryption parameters resolver. */
+ @NonnullAfterInit private DecryptionParametersResolver decryptionParamsResolver;
+
+ /** Strategy used to lookup a per-response {@link DecryptionConfiguration} list. */
+ @NonnullAfterInit private Function<ResponseData,List<DecryptionConfiguration>>
+ decryptionConfigurationLookupStrategy;
+
+ /**
+ * Get the resolver to use for the parameters to store into the context.
+ *
+ * @return the resolver
+ */
+ @NonnullAfterInit DecryptionParametersResolver getDecryptionParametersResolver() {
+ return decryptionParamsResolver;
+ }
+
+ /**
+ * Set the resolver to use for the parameters to store into the context.
+ *
+ * @param resolver the resolver to use
+ */
+ public void setDecryptionParametersResolver(@Nonnull final DecryptionParametersResolver resolver) {
+ checkSetterPreconditions();
+ decryptionParamsResolver = resolver;
+ }
+
+ /**
+ * Get the strategy used to look up a per-response {@link DecryptionConfiguration} list.
+ *
+ * @return the strategy
+ */
+ @NonnullAfterInit Function<ResponseData,List<DecryptionConfiguration>> getDecryptionConfigurationLookupStrategy() {
+ return decryptionConfigurationLookupStrategy;
+ }
+
+ /**
+ * Set the strategy used to look up a per-response {@link DecryptionConfiguration} list.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setDecryptionConfigurationLookupStrategy(
+ @Nonnull final Function<ResponseData,List<DecryptionConfiguration>> strategy) {
+ checkSetterPreconditions();
+ decryptionConfigurationLookupStrategy = strategy;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ if (getDecryptionConfigurationLookupStrategy() == null) {
+ throw new ComponentInitializationException("DecryptionConfiguration lookup strategy was null");
+ }
+ if (getDecryptionParametersResolver() == null) {
+ throw new ComponentInitializationException("DecryptionParametersResolver was null");
+ }
+ }
+
+ /**
+ * Determine if the Response has any EncryptedAsseertion elements.
+ *
+ * @param response the Response to process
+ *
+ * @return true if Response contains EncryptedAssertion elements, otherwise false
+ */
+ public boolean haveEncryptedAssertions(@Nonnull final Response response) {
+ return response.getEncryptedAssertions().size() > 0;
+ }
+
+ /**
+ * Determine if the Response has any EncryptedID or EncryptedAttribute elements.
+ *
+ * @param response the Response to process
+ *
+ * @return true if Response contains EncryptedID or EncryptedAttribute elements, otherwise false
+ */
+ public boolean haveEncryptedContent(@Nonnull final Response response) {
+ for (Assertion assertion : response.getAssertions()) {
+ final Subject subject = assertion.getSubject();
+ if (subject != null && subject.getEncryptedID() != null) {
+ return true;
+ }
+ for (AttributeStatement statement : assertion.getAttributeStatements()) {
+ if (statement.getEncryptedAttributes().size() > 0) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Decrypt the EncryptedAssertion elements of a Response.
+ *
+ * <p>
+ * The resulting decrypted elements are stored in-place in the Response.
+ * </p>
+ *
+ * @param response the Response to process
+ * @param decrypter the decrypter used for decryption operations
+ *
+ * @throws ResolutionException if there is a fatal error during decryption
+ */
+ public void decryptAssertions(@Nonnull final Response response, @Nonnull final Decrypter decrypter)
+ throws ResolutionException {
+ checkComponentActive();
+
+ final Collection<Assertion> decrypteds = new ArrayList<>();
+ final Collection<EncryptedAssertion> encrypteds = new ArrayList<>();
+
+ final Iterator<EncryptedAssertion> i = response.getEncryptedAssertions().iterator();
+ while (i.hasNext()) {
+ log.debug("Decrypting EncryptedAssertion in Response");
+ try {
+ final EncryptedAssertion encrypted = i.next();
+ assert encrypted != null;
+ final Assertion decrypted = decrypter.decrypt(encrypted);
+ if (decrypted != null) {
+ encrypteds.add(encrypted);
+ decrypteds.add(decrypted);
+ }
+ } catch (final DecryptionException e) {
+ throw new ResolutionException("Error decrypting Assertion", e);
+ }
+ }
+
+ response.getEncryptedAssertions().removeAll(encrypteds);
+ response.getAssertions().addAll(decrypteds);
+
+ // Re-marshall the response so that any ID attributes within the decrypted Assertions
+ // will have their ID-ness re-established at the DOM level.
+ if (!decrypteds.isEmpty()) {
+ try {
+ XMLObjectSupport.marshall(response);
+ } catch (final MarshallingException e) {
+ throw new ResolutionException("Error re-marshalling Response after Assertion decryption", e);
+ }
+ }
+ }
+
+ /**
+ * Decrypt the EncryptedID and EncryptedAttribute elements of a Response.
+ *
+ * <p>
+ * The resulting decrypted elements are stored in-place in the Response.
+ * </p>
+ *
+ * @param response the Response to process
+ * @param decrypter the decrypter used for decryption operations
+ *
+ * @throws ResolutionException if there is a fatal error during decryption
+ */
+ public void decryptAssertionContent(@Nonnull final Response response, @Nonnull final Decrypter decrypter)
+ throws ResolutionException {
+
+ for (final Assertion assertion : response.getAssertions()) {
+ if (assertion == null) {
+ continue;
+ }
+ decryptEncryptedIDs(assertion, decrypter);
+ decryptEncryptedAttributes(assertion, decrypter);
+ }
+ }
+
+ /**
+ * Decrypt the EncryptedID elements of an Assertion.
+ *
+ * <p>
+ * The resulting decrypted elements are stored in-place in the Assertion.
+ * </p>
+ *
+ * @param assertion the Assertion to process
+ * @param decrypter the decrypter used for decryption operations
+ *
+ * @throws ResolutionException if there is a fatal error during decryption
+ */
+ public void decryptEncryptedIDs(@Nonnull final Assertion assertion, @Nonnull final Decrypter decrypter)
+ throws ResolutionException {
+ checkComponentActive();
+
+ final Subject subject = assertion.getSubject();
+
+ if (subject != null) {
+ EncryptedID encID = subject.getEncryptedID();
+ if (encID != null) {
+ log.debug("Decrypting EncryptedID in Subject");
+ final NameID decrypted = decryptEncryptedID(encID, decrypter);
+ subject.setNameID(decrypted);
+ subject.setEncryptedID(null);
+ }
+
+ for (final SubjectConfirmation sc : subject.getSubjectConfirmations()) {
+ encID = sc.getEncryptedID();
+ if (encID != null) {
+ log.debug("Decrypting EncryptedID in SubjectConfirmation");
+ final NameID decrypted = decryptEncryptedID(encID, decrypter);
+ sc.setNameID(decrypted);
+ sc.setEncryptedID(null);
+ }
+ }
+ }
+ }
+
+ /**
+ * Decrypt an EncryptedID element.
+ *
+ * @param encID the EncryptedID to process
+ * @param decrypter the decrypter used for decryption operations
+ *
+ * @return the decrypted NameID element
+ *
+ * @throws ResolutionException if there is a fatal error during decryption
+ */
+ @Nonnull public NameID decryptEncryptedID(@Nonnull final EncryptedID encID, @Nonnull final Decrypter decrypter)
+ throws ResolutionException {
+ checkComponentActive();
+
+ try {
+ final SAMLObject object = decrypter.decrypt(encID);
+ if (object instanceof NameID) {
+ return (NameID) object;
+ }
+ throw new DecryptionException("Decrypted EncryptedID was not a NameID, was a "
+ + object.getElementQName().toString());
+ } catch (DecryptionException e) {
+ throw new ResolutionException("Error decryptng EncryptedID", e);
+ }
+ }
+
+ /**
+ * Decrypt the EncryptedAttributes of an Assertion.
+ *
+ * <p>
+ * The resulting decrypted elements are stored in-place in the Assertion.
+ * </p>
+ *
+ * @param assertion the Assertion to process
+ * @param decrypter the decrypter to use
+ *
+ * @throws ResolutionException if there is a fatal error during decryption
+ */
+ public void decryptEncryptedAttributes(@Nonnull final Assertion assertion, @Nonnull final Decrypter decrypter)
+ throws ResolutionException {
+ checkComponentActive();
+
+ for (final AttributeStatement s : assertion.getAttributeStatements()) {
+
+ final Collection<Attribute> decrypteds = new ArrayList<>();
+ final Collection<EncryptedAttribute> encrypteds = new ArrayList<>();
+
+ final Iterator<EncryptedAttribute> i = s.getEncryptedAttributes().iterator();
+ while (i.hasNext()) {
+ log.debug("Decrypting EncryptedAttribute in AttributeStatement");
+
+ try {
+ final EncryptedAttribute encrypted = i.next();
+ assert encrypted != null;
+ final Attribute decrypted = decrypter.decrypt(encrypted);
+ if (decrypted != null) {
+ encrypteds.add(encrypted);
+ decrypteds.add(decrypted);
+ }
+ } catch (final DecryptionException e) {
+ throw new ResolutionException("Error decrypting Attribute", e);
+ }
+ }
+
+ s.getEncryptedAttributes().removeAll(encrypteds);
+ s.getAttributes().addAll(decrypteds);
+ }
+ }
+
+ /**
+ * Build the {@link Decrypter} instance to be used for decryption operations.
+ *
+ * @param responseData the response data being processed
+ *
+ * @return the new Decrypter instance
+ *
+ * @throws ResolutionException if there is a fatal error constructing the Decrypter instance
+ */
+ @Nonnull public Decrypter buildDecrypter(@Nonnull final ResponseData responseData) throws ResolutionException {
+ checkComponentActive();
+
+ final List<DecryptionConfiguration> configs = getDecryptionConfigurationLookupStrategy().apply(responseData);
+ if (configs == null || configs.isEmpty()) {
+ log.error("No DecryptionConfigurations returned by lookup strategy");
+ throw new ResolutionException("No DecryptionConfigurations returned by lookup strategy");
+ }
+
+ try {
+ final CriteriaSet criteria = new CriteriaSet(new DecryptionConfigurationCriterion(configs));
+
+ final DecryptionParameters params = getDecryptionParametersResolver().resolveSingle(criteria);
+ log.debug("{} DecryptionParameters", params != null ? "Resolved" : "Failed to resolve");
+ if (params != null) {
+ return new Decrypter(params);
+ }
+ throw new ResolutionException("Failed to resolve DecryptionParameters");
+ } catch (final ResolverException e) {
+ throw new ResolutionException("Error resolving DecryptionParameters", e);
+ }
+ }
+
+
+}
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/SubjectMatchProcessor.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/SubjectMatchProcessor.java
new file mode 100644
index 000000000..c530395ad
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/SubjectMatchProcessor.java
@@ -0,0 +1,89 @@
+/*
+ * 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.attribute.resolver.dc.saml.util.impl;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.saml.saml2.core.Assertion;
+import org.opensaml.saml.saml2.core.AttributeQuery;
+import org.opensaml.saml.saml2.core.Issuer;
+import org.opensaml.saml.saml2.core.Subject;
+import org.opensaml.saml.saml2.profile.SAML2ObjectSupport;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.attribute.resolver.dc.saml.ResponseData;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * A helper component for matching the {@link Subject} of an {@link Assertion} against the
+ * value sent in the query.
+ */
+public class SubjectMatchProcessor {
+
+ /** Logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(SubjectMatchProcessor.class);
+
+ /**
+ * Match the response Assertions' Subject NameIDs to the one requested in the outgoing query.
+ *
+ * <p>
+ * If an assertion's subject does not match what was requested, it will be removed from further processing.
+ * </p>
+ *
+ * @param responseData the response data being processed
+ */
+ public void process(@Nonnull final ResponseData responseData) {
+ // We know these are all non-null because we created them
+ final MessageContext outboundContext = responseData.getSOAPClientContext().getOutboundMessageContext();
+ assert outboundContext != null;
+ final AttributeQuery query = ((AttributeQuery)outboundContext.getMessage());
+ assert query != null;
+ final Subject querySubject = query.getSubject();
+ assert querySubject != null;
+
+ List<Assertion> toRemove = new LinkedList<>();
+
+ for (final Assertion assertion : responseData.getResponse().getAssertions()) {
+ final Issuer assertionIssuer = assertion.getIssuer();
+
+ final Subject assertionSubject = assertion.getSubject();
+ if (assertionSubject == null) {
+ log.warn("Assertion '{}' from Issuer '{}' did not contain Subject, removing",
+ assertion.getID(),
+ assertionIssuer != null ? assertionIssuer.getValue() : "null");
+ toRemove.add(assertion);
+ continue;
+ }
+
+ if (! SAML2ObjectSupport.matchSubject(assertionSubject, querySubject)) {
+ log.warn("Subject of Assertion '{}' from Issuer '{}' did not match query Subject, removing",
+ assertion.getID(),
+ assertionIssuer != null ? assertionIssuer.getValue() : "null");
+ toRemove.add(assertion);
+ }
+ }
+
+ if (!toRemove.isEmpty()) {
+ responseData.getResponse().getAssertions().removeAll(toRemove);
+ log.debug("Removed a total of {} assertions which failed subject match processing", toRemove.size());
+ }
+
+ }
+
+}
diff --git a/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/package-info.java b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/package-info.java
new file mode 100644
index 000000000..59b1e3ed2
--- /dev/null
+++ b/shib-attribute-resolver-impl/src/main/java/net/shibboleth/idp/attribute/resolver/dc/saml/util/impl/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+/**
+ * Utility support classes used when querying a SAML attribute authority.
+ */
+ at NonnullElements
+package net.shibboleth.idp.attribute.resolver.dc.saml.util.impl;
+
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list