View Javadoc

1   /*
2    * Copyright [2007] [University Corporation for Advanced Internet Development, Inc.]
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.opensaml.security;
18  
19  import java.lang.ref.SoftReference;
20  import java.util.ArrayList;
21  import java.util.Collection;
22  import java.util.HashMap;
23  import java.util.HashSet;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.concurrent.locks.Lock;
27  import java.util.concurrent.locks.ReadWriteLock;
28  import java.util.concurrent.locks.ReentrantReadWriteLock;
29  
30  import javax.xml.namespace.QName;
31  
32  import org.opensaml.Configuration;
33  import org.opensaml.saml2.metadata.KeyDescriptor;
34  import org.opensaml.saml2.metadata.RoleDescriptor;
35  import org.opensaml.saml2.metadata.provider.MetadataProvider;
36  import org.opensaml.saml2.metadata.provider.MetadataProviderException;
37  import org.opensaml.saml2.metadata.provider.ObservableMetadataProvider;
38  import org.opensaml.xml.security.CriteriaSet;
39  import org.opensaml.xml.security.SecurityException;
40  import org.opensaml.xml.security.credential.AbstractCriteriaFilteringCredentialResolver;
41  import org.opensaml.xml.security.credential.BasicCredential;
42  import org.opensaml.xml.security.credential.Credential;
43  import org.opensaml.xml.security.credential.UsageType;
44  import org.opensaml.xml.security.criteria.EntityIDCriteria;
45  import org.opensaml.xml.security.criteria.UsageCriteria;
46  import org.opensaml.xml.security.keyinfo.KeyInfoCredentialResolver;
47  import org.opensaml.xml.security.keyinfo.KeyInfoCriteria;
48  import org.opensaml.xml.util.DatatypeHelper;
49  import org.slf4j.Logger;
50  import org.slf4j.LoggerFactory;
51  
52  /**
53   * A credential resolver capable of resolving credentials from SAML 2 metadata;
54   * 
55   * The instance of {@link CriteriaSet} passed to {@link #resolve(CriteriaSet)} and {@link #resolveSingle(CriteriaSet)}
56   * must minimally contain 2 criteria: {@link EntityIDCriteria} and {@link MetadataCriteria}. The values for
57   * {@link EntityIDCriteria#getEntityID()} and {@link MetadataCriteria#getRole()} are mandatory. If the protocol value
58   * obtained via {@link MetadataCriteria#getProtocol()} is not supplied, credentials will be resolved from all matching
59   * roles, regardless of protocol support. Specification of a {@link UsageCriteria} is optional. If usage criteria is
60   * absent from the criteria set, the effective value {@link UsageType#UNSPECIFIED} will be used for credential
61   * resolution.
62   * 
63   * This credential resolver will cache the resolved the credentials in a memory-sensitive cache. If the metadata
64   * provider is an {@link ObservableMetadataProvider} this resolver will also clear its cache when the underlying
65   * metadata changes.
66   */
67  public class MetadataCredentialResolver extends AbstractCriteriaFilteringCredentialResolver {
68  
69      /** Class logger. */
70      private final Logger log = LoggerFactory.getLogger(MetadataCredentialResolver.class);
71  
72      /** Metadata provider from which to fetch the credentials. */
73      private MetadataProvider metadata;
74  
75      /** Cache of resolved credentials. [MetadataCacheKey, Credentials] */
76      private Map<MetadataCacheKey, SoftReference<Collection<Credential>>> cache;
77  
78      /** Credential resolver used to resolve credentials from role descriptor KeyInfo elements. */
79      private KeyInfoCredentialResolver keyInfoCredentialResolver;
80      
81      /** Lock used to synchronize access to the credential cache. */
82      private ReadWriteLock rwlock;
83  
84      /**
85       * Constructor.
86       * 
87       * @param metadataProvider provider of the metadata
88       * 
89       * @throws IllegalArgumentException thrown if the supplied provider is null
90       */
91      public MetadataCredentialResolver(MetadataProvider metadataProvider) {
92          super();
93          if (metadataProvider == null) {
94              throw new IllegalArgumentException("Metadata provider may not be null");
95          }
96          metadata = metadataProvider;
97  
98          cache = new HashMap<MetadataCacheKey, SoftReference<Collection<Credential>>>();
99  
100         keyInfoCredentialResolver = Configuration.getGlobalSecurityConfiguration()
101                 .getDefaultKeyInfoCredentialResolver();
102         
103         rwlock = new ReentrantReadWriteLock();
104 
105         if (metadata instanceof ObservableMetadataProvider) {
106             ObservableMetadataProvider observable = (ObservableMetadataProvider) metadataProvider;
107             observable.getObservers().add(new MetadataProviderObserver());
108         }
109 
110     }
111 
112     /**
113      * Get the KeyInfo credential resolver used by this metadata resolver to handle KeyInfo elements.
114      * 
115      * @return KeyInfo credential resolver
116      */
117     public KeyInfoCredentialResolver getKeyInfoCredentialResolver() {
118         return keyInfoCredentialResolver;
119     }
120 
121     /**
122      * Set the KeyInfo credential resolver used by this metadata resolver to handle KeyInfo elements.
123      * 
124      * @param keyInfoResolver the new KeyInfoCredentialResolver to use
125      */
126     public void setKeyInfoCredentialResolver(KeyInfoCredentialResolver keyInfoResolver) {
127         keyInfoCredentialResolver = keyInfoResolver;
128     }
129     
130     /**
131      * Get the lock instance used to synchronize access to the credential cache.
132      * 
133      * @return a read-write lock instance
134      */
135     protected ReadWriteLock getReadWriteLock() {
136         return rwlock;
137     }
138 
139     /** {@inheritDoc} */
140     protected Iterable<Credential> resolveFromSource(CriteriaSet criteriaSet) throws SecurityException {
141 
142         checkCriteriaRequirements(criteriaSet);
143 
144         String entityID = criteriaSet.get(EntityIDCriteria.class).getEntityID();
145         MetadataCriteria mdCriteria = criteriaSet.get(MetadataCriteria.class);
146         QName role = mdCriteria.getRole();
147         String protocol = mdCriteria.getProtocol();
148         UsageCriteria usageCriteria = criteriaSet.get(UsageCriteria.class);
149         UsageType usage = null;
150         if (usageCriteria != null) {
151             usage = usageCriteria.getUsage();
152         } else {
153             usage = UsageType.UNSPECIFIED;
154         }
155         
156         // See Jira issue SIDP-229.
157         log.debug("Forcing on-demand metadata provider refresh if necessary");
158         try {
159             metadata.getMetadata();
160         } catch (MetadataProviderException e) {
161             // don't care about errors at this level
162         }
163 
164         MetadataCacheKey cacheKey = new MetadataCacheKey(entityID, role, protocol, usage);
165         Collection<Credential> credentials = retrieveFromCache(cacheKey);
166 
167         if (credentials == null) {
168             credentials = retrieveFromMetadata(entityID, role, protocol, usage);
169             cacheCredentials(cacheKey, credentials);
170         }
171 
172         return credentials;
173     }
174 
175     /**
176      * Check that all necessary credential criteria are available.
177      * 
178      * @param criteriaSet the credential set to evaluate
179      */
180     protected void checkCriteriaRequirements(CriteriaSet criteriaSet) {
181         EntityIDCriteria entityCriteria = criteriaSet.get(EntityIDCriteria.class);
182         MetadataCriteria mdCriteria = criteriaSet.get(MetadataCriteria.class);
183         if (entityCriteria == null) {
184             throw new IllegalArgumentException("Entity criteria must be supplied");
185         }
186         if (mdCriteria == null) {
187             throw new IllegalArgumentException("SAML metadata criteria must be supplied");
188         }
189         if (DatatypeHelper.isEmpty(entityCriteria.getEntityID())) {
190             throw new IllegalArgumentException("Credential owner entity ID criteria value must be supplied");
191         }
192         if (mdCriteria.getRole() == null) {
193             throw new IllegalArgumentException("Credential metadata role criteria value must be supplied");
194         }
195     }
196 
197     /**
198      * Retrieves pre-resolved credentials from the cache.
199      * 
200      * @param cacheKey the key to the metadata cache
201      * 
202      * @return the collection of cached credentials or null
203      */
204     protected Collection<Credential> retrieveFromCache(MetadataCacheKey cacheKey) {
205         log.debug("Attempting to retrieve credentials from cache using index: {}", cacheKey);
206         Lock readLock = getReadWriteLock().readLock();
207         readLock.lock();
208         log.trace("Read lock over cache acquired");
209         try {
210             if (cache.containsKey(cacheKey)) {
211                 SoftReference<Collection<Credential>> reference = cache.get(cacheKey);
212                 if (reference.get() != null) {
213                     log.debug("Retrieved credentials from cache using index: {}", cacheKey);
214                     return reference.get();
215                 }
216             }
217         } finally {
218             readLock.unlock();
219             log.trace("Read lock over cache released");
220         }
221 
222         log.debug("Unable to retrieve credentials from cache using index: {}", cacheKey);
223         return null;
224     }
225 
226     /**
227      * Retrieves credentials from the provided metadata.
228      * 
229      * @param entityID entityID of the credential owner
230      * @param role role in which the entity is operating
231      * @param protocol protocol over which the entity is operating (may be null)
232      * @param usage intended usage of resolved credentials
233      * 
234      * @return the resolved credentials or null
235      * 
236      * @throws SecurityException thrown if the key, certificate, or CRL information is represented in an unsupported
237      *             format
238      */
239     protected Collection<Credential> retrieveFromMetadata(String entityID, QName role, String protocol, UsageType usage)
240             throws SecurityException {
241 
242         log.debug("Attempting to retrieve credentials from metadata for entity: {}", entityID);
243         Collection<Credential> credentials = new HashSet<Credential>(3);
244 
245         List<RoleDescriptor> roleDescriptors = getRoleDescriptors(entityID, role, protocol);
246         if(roleDescriptors == null || roleDescriptors.isEmpty()){
247             return credentials;
248         }
249             
250         for (RoleDescriptor roleDescriptor : roleDescriptors) {
251             List<KeyDescriptor> keyDescriptors = roleDescriptor.getKeyDescriptors();
252             if(keyDescriptors == null || keyDescriptors.isEmpty()){
253                 return credentials;
254             }            
255             for (KeyDescriptor keyDescriptor : keyDescriptors) {
256                 UsageType mdUsage = keyDescriptor.getUse();
257                 if (mdUsage == null) {
258                     mdUsage = UsageType.UNSPECIFIED;
259                 }
260                 if (matchUsage(mdUsage, usage)) {
261                     if (keyDescriptor.getKeyInfo() != null) {
262                         CriteriaSet critSet = new CriteriaSet();
263                         critSet.add(new KeyInfoCriteria(keyDescriptor.getKeyInfo()));
264 
265                         Iterable<Credential> creds = getKeyInfoCredentialResolver().resolve(critSet);
266                         if(credentials == null){
267                             continue;
268                         }
269                         for (Credential cred : creds) {
270                             if (cred instanceof BasicCredential) {
271                                 BasicCredential basicCred = (BasicCredential) cred;
272                                 basicCred.setEntityId(entityID);
273                                 basicCred.setUsageType(mdUsage);
274                                 basicCred.getCredentalContextSet().add(new SAMLMDCredentialContext(keyDescriptor));
275                             }
276                             credentials.add(cred);
277                         }
278                     }
279                 }
280             }
281 
282         }
283 
284         return credentials;
285     }
286 
287     /**
288      * Match usage enum type values from metadata KeyDescriptor and from credential criteria.
289      * 
290      * @param metadataUsage the value from the 'use' attribute of a metadata KeyDescriptor element
291      * @param criteriaUsage the value from credential criteria
292      * @return true if the two usage specifiers match for purposes of resolving credentials, false otherwise
293      */
294     protected boolean matchUsage(UsageType metadataUsage, UsageType criteriaUsage) {
295         if (metadataUsage == UsageType.UNSPECIFIED || criteriaUsage == UsageType.UNSPECIFIED) {
296             return true;
297         }
298         return metadataUsage == criteriaUsage;
299     }
300 
301     /**
302      * Get the list of metadata role descriptors which match the given entityID, role and protocol.
303      * 
304      * @param entityID entity ID of the credential owner
305      * @param role role in which the entity is operating
306      * @param protocol protocol over which the entity is operating (may be null)
307      * @return a list of role descriptors matching the given parameters, or null
308      * @throws SecurityException thrown if there is an error retrieving role descriptors from the metadata provider
309      */
310     protected List<RoleDescriptor> getRoleDescriptors(String entityID, QName role, String protocol)
311             throws SecurityException {
312         try {
313             if (log.isDebugEnabled()) {
314                 log.debug("Retrieving metadata for entity '{}' in role '{}' for protocol '{}'", 
315                         new Object[] {entityID, role, protocol});
316             }
317 
318             if (DatatypeHelper.isEmpty(protocol)) {
319                 return metadata.getRole(entityID, role);
320             } else {
321                 RoleDescriptor roleDescriptor = metadata.getRole(entityID, role, protocol);
322                 if (roleDescriptor == null) {
323                     return null;
324                 }
325                 List<RoleDescriptor> roles = new ArrayList<RoleDescriptor>();
326                 roles.add(roleDescriptor);
327                 return roles;
328             }
329         } catch (MetadataProviderException e) {
330             log.error("Unable to read metadata from provider", e);
331             throw new SecurityException("Unable to read metadata provider", e);
332         }
333     }
334 
335     /**
336      * Adds resolved credentials to the cache.
337      * 
338      * @param cacheKey the key for caching the credentials
339      * @param credentials collection of credentials to cache
340      */
341     protected void cacheCredentials(MetadataCacheKey cacheKey, Collection<Credential> credentials) {
342         Lock writeLock = getReadWriteLock().writeLock();
343         writeLock.lock();
344         log.trace("Write lock over cache acquired");
345         try {
346             cache.put(cacheKey, new SoftReference<Collection<Credential>>(credentials));
347             log.debug("Added new credential collection to cache with key: {}", cacheKey);
348         } finally {
349             writeLock.unlock();
350             log.trace("Write lock over cache released"); 
351         }
352     }
353 
354     /**
355      * A class which serves as the key into the cache of credentials previously resolved.
356      */
357     protected class MetadataCacheKey {
358 
359         /** Entity ID of credential owner. */
360         private String id;
361 
362         /** Role in which the entity is operating. */
363         private QName role;
364 
365         /** Protocol over which the entity is operating (may be null). */
366         private String protocol;
367 
368         /** Intended usage of the resolved credentials. */
369         private UsageType usage;
370 
371         /**
372          * Constructor.
373          * 
374          * @param entityID entity ID of the credential owner
375          * @param entityRole role in which the entity is operating
376          * @param entityProtocol protocol over which the entity is operating (may be null)
377          * @param entityUsage usage of the resolved credentials
378          */
379         protected MetadataCacheKey(String entityID, QName entityRole, String entityProtocol, UsageType entityUsage) {
380             if (entityID == null) {
381                 throw new IllegalArgumentException("Entity ID may not be null");
382             }
383             if (entityRole == null) {
384                 throw new IllegalArgumentException("Entity role may not be null");
385             }
386             if (entityUsage == null) {
387                 throw new IllegalArgumentException("Credential usage may not be null");
388             }
389             id = entityID;
390             role = entityRole;
391             protocol = entityProtocol;
392             usage = entityUsage;
393         }
394 
395         /** {@inheritDoc} */
396         public boolean equals(Object obj) {
397             if (obj == this) {
398                 return true;
399             }
400             if (!(obj instanceof MetadataCacheKey)) {
401                 return false;
402             }
403             MetadataCacheKey other = (MetadataCacheKey) obj;
404             if (!this.id.equals(other.id) || !this.role.equals(other.role) || this.usage != other.usage) {
405                 return false;
406             }
407             if (this.protocol == null) {
408                 if (other.protocol != null) {
409                     return false;
410                 }
411             } else {
412                 if (!this.protocol.equals(other.protocol)) {
413                     return false;
414                 }
415             }
416             return true;
417         }
418 
419         /** {@inheritDoc} */
420         public int hashCode() {
421             int result = 17;
422             result = 37 * result + id.hashCode();
423             result = 37 * result + role.hashCode();
424             if (protocol != null) {
425                 result = 37 * result + protocol.hashCode();
426             }
427             result = 37 * result + usage.hashCode();
428             return result;
429         }
430 
431         /** {@inheritDoc} */
432         public String toString() {
433             return String.format("[%s,%s,%s,%s]", id, role, protocol, usage);
434         }
435 
436     }
437 
438     /**
439      * An observer that clears the credential cache if the underlying metadata changes.
440      */
441     protected class MetadataProviderObserver implements ObservableMetadataProvider.Observer {
442 
443         /** {@inheritDoc} */
444         public void onEvent(MetadataProvider provider) {
445             Lock writeLock = getReadWriteLock().writeLock();
446             writeLock.lock();
447             log.trace("Write lock over cache acquired");
448             try {
449                 cache.clear();
450                 log.debug("Credential cache cleared");
451             } finally {
452                 writeLock.unlock();
453                 log.trace("Write lock over cache released"); 
454             }
455         }
456     }
457 }