org.opensaml.security.MetadataCredentialResolver 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.security;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import javax.xml.namespace.QName;
import org.opensaml.Configuration;
import org.opensaml.saml2.metadata.KeyDescriptor;
import org.opensaml.saml2.metadata.RoleDescriptor;
import org.opensaml.saml2.metadata.provider.MetadataProvider;
import org.opensaml.saml2.metadata.provider.MetadataProviderException;
import org.opensaml.saml2.metadata.provider.ObservableMetadataProvider;
import org.opensaml.xml.security.CriteriaSet;
import org.opensaml.xml.security.SecurityException;
import org.opensaml.xml.security.credential.AbstractCriteriaFilteringCredentialResolver;
import org.opensaml.xml.security.credential.BasicCredential;
import org.opensaml.xml.security.credential.Credential;
import org.opensaml.xml.security.credential.UsageType;
import org.opensaml.xml.security.criteria.EntityIDCriteria;
import org.opensaml.xml.security.criteria.UsageCriteria;
import org.opensaml.xml.security.keyinfo.KeyInfoCredentialResolver;
import org.opensaml.xml.security.keyinfo.KeyInfoCriteria;
import org.opensaml.xml.util.DatatypeHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A credential resolver capable of resolving credentials from SAML 2 metadata;
*
* The instance of {@link CriteriaSet} passed to {@link #resolve(CriteriaSet)} and {@link #resolveSingle(CriteriaSet)}
* must minimally contain 2 criteria: {@link EntityIDCriteria} and {@link MetadataCriteria}. The values for
* {@link EntityIDCriteria#getEntityID()} and {@link MetadataCriteria#getRole()} are mandatory. If the protocol value
* obtained via {@link MetadataCriteria#getProtocol()} is not supplied, credentials will be resolved from all matching
* roles, regardless of protocol support. Specification of a {@link UsageCriteria} is optional. If usage criteria is
* absent from the criteria set, the effective value {@link UsageType#UNSPECIFIED} will be used for credential
* resolution.
*
* This credential resolver will cache the resolved the credentials in a memory-sensitive cache. If the metadata
* provider is an {@link ObservableMetadataProvider} this resolver will also clear its cache when the underlying
* metadata changes.
*/
public class MetadataCredentialResolver extends AbstractCriteriaFilteringCredentialResolver {
/** Class logger. */
private final Logger log = LoggerFactory.getLogger(MetadataCredentialResolver.class);
/** Metadata provider from which to fetch the credentials. */
private MetadataProvider metadata;
/** Cache of resolved credentials. [MetadataCacheKey, Credentials] */
private Map>> cache;
/** Credential resolver used to resolve credentials from role descriptor KeyInfo elements. */
private KeyInfoCredentialResolver keyInfoCredentialResolver;
/** Lock used to synchronize access to the credential cache. */
private ReadWriteLock rwlock;
/**
* Constructor.
*
* @param metadataProvider provider of the metadata
*
* @throws IllegalArgumentException thrown if the supplied provider is null
*/
public MetadataCredentialResolver(MetadataProvider metadataProvider) {
super();
if (metadataProvider == null) {
throw new IllegalArgumentException("Metadata provider may not be null");
}
metadata = metadataProvider;
cache = new HashMap>>();
keyInfoCredentialResolver = Configuration.getGlobalSecurityConfiguration()
.getDefaultKeyInfoCredentialResolver();
rwlock = new ReentrantReadWriteLock();
if (metadata instanceof ObservableMetadataProvider) {
ObservableMetadataProvider observable = (ObservableMetadataProvider) metadataProvider;
observable.getObservers().add(new MetadataProviderObserver());
}
}
/**
* Get the metadata provider instance used by this resolver.
*
* @return the resolver's metadata provider instance
*/
public MetadataProvider getMetadataProvider() {
return metadata;
}
/**
* Get the KeyInfo credential resolver used by this metadata resolver to handle KeyInfo elements.
*
* @return KeyInfo credential resolver
*/
public KeyInfoCredentialResolver getKeyInfoCredentialResolver() {
return keyInfoCredentialResolver;
}
/**
* Set the KeyInfo credential resolver used by this metadata resolver to handle KeyInfo elements.
*
* @param keyInfoResolver the new KeyInfoCredentialResolver to use
*/
public void setKeyInfoCredentialResolver(KeyInfoCredentialResolver keyInfoResolver) {
keyInfoCredentialResolver = keyInfoResolver;
}
/**
* Get the lock instance used to synchronize access to the credential cache.
*
* @return a read-write lock instance
*/
protected ReadWriteLock getReadWriteLock() {
return rwlock;
}
/** {@inheritDoc} */
protected Iterable resolveFromSource(CriteriaSet criteriaSet) throws SecurityException {
checkCriteriaRequirements(criteriaSet);
String entityID = criteriaSet.get(EntityIDCriteria.class).getEntityID();
MetadataCriteria mdCriteria = criteriaSet.get(MetadataCriteria.class);
QName role = mdCriteria.getRole();
String protocol = mdCriteria.getProtocol();
UsageCriteria usageCriteria = criteriaSet.get(UsageCriteria.class);
UsageType usage = null;
if (usageCriteria != null) {
usage = usageCriteria.getUsage();
} else {
usage = UsageType.UNSPECIFIED;
}
// See Jira issue SIDP-229.
log.debug("Forcing on-demand metadata provider refresh if necessary");
try {
metadata.getMetadata();
} catch (MetadataProviderException e) {
// don't care about errors at this level
}
MetadataCacheKey cacheKey = new MetadataCacheKey(entityID, role, protocol, usage);
Collection credentials = retrieveFromCache(cacheKey);
if (credentials == null) {
credentials = retrieveFromMetadata(entityID, role, protocol, usage);
cacheCredentials(cacheKey, credentials);
}
return credentials;
}
/**
* Check that all necessary credential criteria are available.
*
* @param criteriaSet the credential set to evaluate
*/
protected void checkCriteriaRequirements(CriteriaSet criteriaSet) {
EntityIDCriteria entityCriteria = criteriaSet.get(EntityIDCriteria.class);
MetadataCriteria mdCriteria = criteriaSet.get(MetadataCriteria.class);
if (entityCriteria == null) {
throw new IllegalArgumentException("Entity criteria must be supplied");
}
if (mdCriteria == null) {
throw new IllegalArgumentException("SAML metadata criteria must be supplied");
}
if (DatatypeHelper.isEmpty(entityCriteria.getEntityID())) {
throw new IllegalArgumentException("Credential owner entity ID criteria value must be supplied");
}
if (mdCriteria.getRole() == null) {
throw new IllegalArgumentException("Credential metadata role criteria value must be supplied");
}
}
/**
* Retrieves pre-resolved credentials from the cache.
*
* @param cacheKey the key to the metadata cache
*
* @return the collection of cached credentials or null
*/
protected Collection retrieveFromCache(MetadataCacheKey cacheKey) {
log.debug("Attempting to retrieve credentials from cache using index: {}", cacheKey);
Lock readLock = getReadWriteLock().readLock();
readLock.lock();
log.trace("Read lock over cache acquired");
try {
if (cache.containsKey(cacheKey)) {
SoftReference> reference = cache.get(cacheKey);
if (reference.get() != null) {
log.debug("Retrieved credentials from cache using index: {}", cacheKey);
return reference.get();
}
}
} finally {
readLock.unlock();
log.trace("Read lock over cache released");
}
log.debug("Unable to retrieve credentials from cache using index: {}", cacheKey);
return null;
}
/**
* Retrieves credentials from the provided metadata.
*
* @param entityID entityID of the credential owner
* @param role role in which the entity is operating
* @param protocol protocol over which the entity is operating (may be null)
* @param usage intended usage of resolved credentials
*
* @return the resolved credentials or null
*
* @throws SecurityException thrown if the key, certificate, or CRL information is represented in an unsupported
* format
*/
protected Collection retrieveFromMetadata(String entityID, QName role, String protocol, UsageType usage)
throws SecurityException {
log.debug("Attempting to retrieve credentials from metadata for entity: {}", entityID);
Collection credentials = new HashSet(3);
List roleDescriptors = getRoleDescriptors(entityID, role, protocol);
if(roleDescriptors == null || roleDescriptors.isEmpty()){
return credentials;
}
for (RoleDescriptor roleDescriptor : roleDescriptors) {
List keyDescriptors = roleDescriptor.getKeyDescriptors();
if(keyDescriptors == null || keyDescriptors.isEmpty()){
return credentials;
}
for (KeyDescriptor keyDescriptor : keyDescriptors) {
UsageType mdUsage = keyDescriptor.getUse();
if (mdUsage == null) {
mdUsage = UsageType.UNSPECIFIED;
}
if (matchUsage(mdUsage, usage)) {
if (keyDescriptor.getKeyInfo() != null) {
CriteriaSet critSet = new CriteriaSet();
critSet.add(new KeyInfoCriteria(keyDescriptor.getKeyInfo()));
Iterable creds = getKeyInfoCredentialResolver().resolve(critSet);
if(credentials == null){
continue;
}
for (Credential cred : creds) {
if (cred instanceof BasicCredential) {
BasicCredential basicCred = (BasicCredential) cred;
basicCred.setEntityId(entityID);
basicCred.setUsageType(mdUsage);
basicCred.getCredentalContextSet().add(new SAMLMDCredentialContext(keyDescriptor));
}
credentials.add(cred);
}
}
}
}
}
return credentials;
}
/**
* Match usage enum type values from metadata KeyDescriptor and from credential criteria.
*
* @param metadataUsage the value from the 'use' attribute of a metadata KeyDescriptor element
* @param criteriaUsage the value from credential criteria
* @return true if the two usage specifiers match for purposes of resolving credentials, false otherwise
*/
protected boolean matchUsage(UsageType metadataUsage, UsageType criteriaUsage) {
if (metadataUsage == UsageType.UNSPECIFIED || criteriaUsage == UsageType.UNSPECIFIED) {
return true;
}
return metadataUsage == criteriaUsage;
}
/**
* Get the list of metadata role descriptors which match the given entityID, role and protocol.
*
* @param entityID entity ID of the credential owner
* @param role role in which the entity is operating
* @param protocol protocol over which the entity is operating (may be null)
* @return a list of role descriptors matching the given parameters, or null
* @throws SecurityException thrown if there is an error retrieving role descriptors from the metadata provider
*/
protected List getRoleDescriptors(String entityID, QName role, String protocol)
throws SecurityException {
try {
if (log.isDebugEnabled()) {
log.debug("Retrieving metadata for entity '{}' in role '{}' for protocol '{}'",
new Object[] {entityID, role, protocol});
}
if (DatatypeHelper.isEmpty(protocol)) {
return metadata.getRole(entityID, role);
} else {
RoleDescriptor roleDescriptor = metadata.getRole(entityID, role, protocol);
if (roleDescriptor == null) {
return null;
}
List roles = new ArrayList();
roles.add(roleDescriptor);
return roles;
}
} catch (MetadataProviderException e) {
log.error("Unable to read metadata from provider", e);
throw new SecurityException("Unable to read metadata provider", e);
}
}
/**
* Adds resolved credentials to the cache.
*
* @param cacheKey the key for caching the credentials
* @param credentials collection of credentials to cache
*/
protected void cacheCredentials(MetadataCacheKey cacheKey, Collection credentials) {
Lock writeLock = getReadWriteLock().writeLock();
writeLock.lock();
log.trace("Write lock over cache acquired");
try {
cache.put(cacheKey, new SoftReference>(credentials));
log.debug("Added new credential collection to cache with key: {}", cacheKey);
} finally {
writeLock.unlock();
log.trace("Write lock over cache released");
}
}
/**
* A class which serves as the key into the cache of credentials previously resolved.
*/
protected class MetadataCacheKey {
/** Entity ID of credential owner. */
private String id;
/** Role in which the entity is operating. */
private QName role;
/** Protocol over which the entity is operating (may be null). */
private String protocol;
/** Intended usage of the resolved credentials. */
private UsageType usage;
/**
* Constructor.
*
* @param entityID entity ID of the credential owner
* @param entityRole role in which the entity is operating
* @param entityProtocol protocol over which the entity is operating (may be null)
* @param entityUsage usage of the resolved credentials
*/
protected MetadataCacheKey(String entityID, QName entityRole, String entityProtocol, UsageType entityUsage) {
if (entityID == null) {
throw new IllegalArgumentException("Entity ID may not be null");
}
if (entityRole == null) {
throw new IllegalArgumentException("Entity role may not be null");
}
if (entityUsage == null) {
throw new IllegalArgumentException("Credential usage may not be null");
}
id = entityID;
role = entityRole;
protocol = entityProtocol;
usage = entityUsage;
}
/** {@inheritDoc} */
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof MetadataCacheKey)) {
return false;
}
MetadataCacheKey other = (MetadataCacheKey) obj;
if (!this.id.equals(other.id) || !this.role.equals(other.role) || this.usage != other.usage) {
return false;
}
if (this.protocol == null) {
if (other.protocol != null) {
return false;
}
} else {
if (!this.protocol.equals(other.protocol)) {
return false;
}
}
return true;
}
/** {@inheritDoc} */
public int hashCode() {
int result = 17;
result = 37 * result + id.hashCode();
result = 37 * result + role.hashCode();
if (protocol != null) {
result = 37 * result + protocol.hashCode();
}
result = 37 * result + usage.hashCode();
return result;
}
/** {@inheritDoc} */
public String toString() {
return String.format("[%s,%s,%s,%s]", id, role, protocol, usage);
}
}
/**
* An observer that clears the credential cache if the underlying metadata changes.
*/
protected class MetadataProviderObserver implements ObservableMetadataProvider.Observer {
/** {@inheritDoc} */
public void onEvent(MetadataProvider provider) {
Lock writeLock = getReadWriteLock().writeLock();
writeLock.lock();
log.trace("Write lock over cache acquired");
try {
cache.clear();
log.debug("Credential cache cleared");
} finally {
writeLock.unlock();
log.trace("Write lock over cache released");
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy