org.opensaml.saml.common.binding.AbstractEndpointResolver Maven / Gradle / Ivy
/*
* Licensed to the University Corporation for Advanced Internet Development,
* Inc. (UCAID) under one or more contributor license agreements. See the
* NOTICE file distributed with this work for additional information regarding
* copyright ownership. The UCAID licenses this file to You under the Apache
* License, Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.opensaml.saml.common.binding;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.xml.namespace.QName;
import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
import net.shibboleth.utilities.java.support.component.AbstractIdentifiedInitializableComponent;
import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
import net.shibboleth.utilities.java.support.resolver.ResolverException;
import org.opensaml.saml.criterion.EndpointCriterion;
import org.opensaml.saml.criterion.RoleDescriptorCriterion;
import org.opensaml.saml.saml2.metadata.Endpoint;
import org.opensaml.saml.saml2.metadata.IndexedEndpoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Base implementation that resolves and validates protocol/profile endpoints using a combination of supplied
* parameters and SAML metadata.
*
* SAML metadata rules are followed for deriving candidate endpoints to evaluate. The base class implements
* only a subset of required functionality, then extracts a set of candidates from metadata if present, and
* delegates to a subclass to actually evaluate each one for acceptability.
*
* The supported {@link net.shibboleth.utilities.java.support.resolver.Criterion} types and their use follows:
*
*
* - {@link EndpointCriterion} (required)
*
- Contains a "template" for the eventual {@link Endpoint}(s) to resolve that identifies at minimum the
* type of endpoint object (via schema type or element name) to resolve. It MAY contain other attributes that
* will be used in matching candidate endpoints for suitability, such as index, binding, location, etc. If so
* marked, it may also be resolved as a trusted endpoint without additional verification required.
*
*
- {@link RoleDescriptorCriterion}
*
- If present, provides access to the candidate endpoint(s) to attempt resolution against. Strictly optional,
* but if absent, the supplied endpoint (from {@link EndpointCriterion}) is returned as the sole result,
* whatever its completeness/usability, allowing for subclass validation.
*
*
* Subclasses should override the {{@link #doCheckEndpoint(CriteriaSet, Endpoint)} method to implement
* further criteria.
*
* @param type of endpoint
*/
public abstract class AbstractEndpointResolver
extends AbstractIdentifiedInitializableComponent implements EndpointResolver {
/** Class logger. */
@Nonnull private Logger log = LoggerFactory.getLogger(AbstractEndpointResolver.class);
/** Constructor. */
public AbstractEndpointResolver() {
super.setId(getClass().getName());
}
/** {@inheritDoc} */
@Override
@Nonnull @NonnullElements public Iterable resolve(@Nullable final CriteriaSet criteria)
throws ResolverException {
validateCriteria(criteria);
if (canUseRequestedEndpoint(criteria)) {
final EndpointType endpoint = (EndpointType) criteria.get(EndpointCriterion.class).getEndpoint();
if (doCheckEndpoint(criteria, endpoint)) {
return Collections.singletonList(endpoint);
} else {
log.debug("{} Requested endpoint was rejected by extended validation process", getLogPrefix());
return Collections.emptyList();
}
}
final List candidates = getCandidatesFromMetadata(criteria);
final Iterator i = candidates.iterator();
while (i.hasNext()) {
if (!doCheckEndpoint(criteria, i.next())) {
i.remove();
}
}
log.debug("{} {} endpoints remain after filtering process", getLogPrefix(), candidates.size());
return candidates;
}
/** {@inheritDoc} */
@Override
@Nullable public EndpointType resolveSingle(@Nullable final CriteriaSet criteria) throws ResolverException {
validateCriteria(criteria);
if (canUseRequestedEndpoint(criteria)) {
final EndpointType endpoint = (EndpointType) criteria.get(EndpointCriterion.class).getEndpoint();
if (doCheckEndpoint(criteria, endpoint)) {
return endpoint;
} else {
log.debug("{} Requested endpoint was rejected by extended validation process", getLogPrefix());
return null;
}
}
for (final EndpointType candidate : getCandidatesFromMetadata(criteria)) {
if (doCheckEndpoint(criteria, candidate)) {
return candidate;
}
}
log.debug("{} No candidate endpoints met criteria", getLogPrefix());
return null;
}
/**
* Apply the supplied criteria to a candidate endpoint to determine its suitability.
*
* @param criteria input criteria set
* @param endpoint candidate endpoint
*
* @return true iff the endpoint meets the supplied criteria
*/
protected boolean doCheckEndpoint(@Nonnull final CriteriaSet criteria, @Nonnull final EndpointType endpoint) {
return true;
}
/**
* Verify that the required {@link EndpointCriterion} is present.
*
* @param criteria input criteria set
*
* @throws ResolverException if the input set is null or no {@link EndpointCriterion} is present
*/
private void validateCriteria(@Nullable final CriteriaSet criteria) throws ResolverException {
if (criteria == null) {
throw new ResolverException("CriteriaSet cannot be null");
}
final EndpointCriterion epCriterion = criteria.get(EndpointCriterion.class);
if (epCriterion == null) {
throw new ResolverException("EndpointCriterion not supplied");
}
}
/**
* Optimize the case of resolving a single endpoint if a populated endpoint is supplied via
* criteria, and validation is unnecessary due to a signed request. Note that this endpoint may
* turn out to be unusable by the caller, but that's immaterial because the requester must have
* dictated the binding and location, so we're not allowed to ignore that.
*
* @param criteria input criteria set
*
* @return true iff the supplied endpoint via {@link EndpointCriterion} should be returned
*/
private boolean canUseRequestedEndpoint(@Nonnull final CriteriaSet criteria) {
final EndpointCriterion epc = criteria.get(EndpointCriterion.class);
if (epc.isTrusted()) {
final EndpointType requestedEndpoint = (EndpointType) epc.getEndpoint();
if (requestedEndpoint.getBinding() != null && (requestedEndpoint.getLocation() != null
|| requestedEndpoint.getResponseLocation() != null)) {
return true;
}
}
return false;
}
/**
* Get a mutable list of endpoints of a given type found in the metadata role contained in a
* {@link RoleDescriptorCriterion} (or an empty list if no metadata exists).
*
* The endpoint type to extract is based on the candidate endpoint in an
* {@link EndpointCriterion}. If the endpoints are indexed, the first list entry will
* contain the default endpoint to use in the absence of other limiting criteria.
*
* @param criteria input criteria set
*
* @return mutable list of endpoints from the metadata
*/
@Nonnull @NonnullElements private List getCandidatesFromMetadata(
@Nonnull final CriteriaSet criteria) {
// Check for metadata.
final RoleDescriptorCriterion role = criteria.get(RoleDescriptorCriterion.class);
if (role == null) {
log.debug("{} No metadata supplied, no candidate endpoints to return", getLogPrefix());
return new ArrayList<>();
}
// Determine the QName type of endpoints to extract based on candidate type.
final EndpointCriterion epCriterion = criteria.get(EndpointCriterion.class);
QName endpointType = epCriterion.getEndpoint().getSchemaType();
if (endpointType == null) {
endpointType = epCriterion.getEndpoint().getElementQName();
}
// Return the endpoints in the metadata of the candidate type.
final List endpoints = role.getRole().getEndpoints(endpointType);
if (endpoints.isEmpty()) {
log.debug("{} No endpoints in metadata of type {}", getLogPrefix(), endpointType);
} else {
log.debug("{} Returning {} candidate endpoints of type {}", getLogPrefix(), endpoints.size(),
endpointType);
}
return sortCandidates(endpoints);
}
/**
* Copy and sort the endpoints such that the default endpoint by SAML rules comes first.
*
* @param candidates input list of endpoints
*
* @return a new list containing the endpoints such that the default is first
*/
// Checkstyle: CyclomaticComplexity OFF
@Nonnull @NonnullElements private List sortCandidates(
@Nonnull @NonnullElements List candidates) {
// Use a linked list, and move the default endpoint to the head of the list.
// SAML defaulting rules apply to IndexedEnpdoint types, and require checking
// for the isDefault attribute. The default is the one marked true, or if none are,
// the first not marked false.
EndpointType hardDefault = null;
EndpointType softDefault = null;
final LinkedList toReturn = new LinkedList();
for (final Endpoint endpoint : candidates) {
if (hardDefault == null && endpoint instanceof IndexedEndpoint) {
final Boolean flag = ((IndexedEndpoint) endpoint).isDefault();
if (flag != null) {
if (flag.booleanValue()) {
hardDefault = (EndpointType) endpoint;
if (softDefault != null) {
toReturn.addFirst(softDefault);
softDefault = null;
}
} else {
toReturn.addLast((EndpointType) endpoint);
}
} else if (hardDefault == null && softDefault == null) {
softDefault = (EndpointType) endpoint;
} else {
toReturn.addLast((EndpointType) endpoint);
}
} else {
toReturn.addLast((EndpointType) endpoint);
}
}
if (hardDefault != null) {
toReturn.addFirst(hardDefault);
} else if (softDefault != null) {
toReturn.addFirst(softDefault);
}
return toReturn;
}
// Checkstyle: CyclomaticComplexity ON
/**
* Return a prefix for logging messages for this component.
*
* @return a string for insertion at the beginning of any log messages
*/
@Nonnull protected String getLogPrefix() {
return "Endpoint Resolver " + getId() + ":";
}
}