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 edu.internet2.middleware.shibboleth.common.attribute.resolver.provider.dataConnector;
18  
19  import java.security.MessageDigest;
20  import java.security.NoSuchAlgorithmException;
21  import java.sql.SQLException;
22  import java.sql.Timestamp;
23  import java.util.Collection;
24  import java.util.Map;
25  import java.util.UUID;
26  
27  import javax.sql.DataSource;
28  
29  import org.opensaml.saml2.core.AuthnRequest;
30  import org.opensaml.xml.XMLObject;
31  import org.opensaml.xml.util.Base64;
32  import org.opensaml.xml.util.DatatypeHelper;
33  import org.opensaml.xml.util.LazyMap;
34  import org.slf4j.Logger;
35  import org.slf4j.LoggerFactory;
36  
37  import edu.internet2.middleware.shibboleth.common.attribute.BaseAttribute;
38  import edu.internet2.middleware.shibboleth.common.attribute.provider.BasicAttribute;
39  import edu.internet2.middleware.shibboleth.common.attribute.resolver.AttributeResolutionException;
40  import edu.internet2.middleware.shibboleth.common.attribute.resolver.provider.ShibbolethResolutionContext;
41  import edu.internet2.middleware.shibboleth.common.attribute.resolver.provider.dataConnector.StoredIDStore.PersistentIdEntry;
42  
43  /**
44   * A data connector that generates persistent identifiers in one of two ways. The generated attribute has an ID of
45   * <tt>peristentId</tt> and contains a single {@link String} value.
46   * 
47   * If a salt is supplied at construction time the generated IDs will be the Base64-encoded SHA-1 hash of the user's
48   * principal name, the peer entity ID, and the salt.
49   * 
50   * If a {@link DataSource} is supplied the IDs are created and managed as described by {@link StoredIDStore}.
51   */
52  public class StoredIDDataConnector extends BaseDataConnector {
53  
54      /** Class logger. */
55      private final Logger log = LoggerFactory.getLogger(StoredIDDataConnector.class);
56  
57      /** Persistent identifier data store. */
58      private StoredIDStore pidStore;
59  
60      /** ID of the attribute generated by this data connector. */
61      private String generatedAttribute;
62  
63      /** ID of the attribute whose first value is used when generating the computed ID. */
64      private String sourceAttribute;
65  
66      /** Salt used when computing the ID. */
67      private byte[] salt;
68  
69      /**
70       * Constructor.
71       * 
72       * @param source datasource used to communicate with the database
73       * @param queryTimeout SQL qeury timeout in seconds
74       * @param generatedAttributeId ID of the attribute generated by this data connector
75       * @param sourceAttributeId ID of the attribute whose first value is used when generating the computed ID
76       * @param idSalt salt used when computing the ID
77       */
78      public StoredIDDataConnector(DataSource source, int queryTimeout, String generatedAttributeId,
79              String sourceAttributeId, byte[] idSalt) {
80          if (source == null) {
81              throw new IllegalArgumentException("Data source may not be null");
82          }
83          pidStore = new StoredIDStore(source, queryTimeout);
84  
85          if (DatatypeHelper.isEmpty(generatedAttributeId)) {
86              throw new IllegalArgumentException("Provided generated attribute ID must not be empty");
87          }
88          generatedAttribute = generatedAttributeId;
89  
90          if (DatatypeHelper.isEmpty(sourceAttributeId)) {
91              throw new IllegalArgumentException("Provided source attribute ID must not be empty");
92          }
93          sourceAttribute = sourceAttributeId;
94  
95          if (idSalt.length < 16) {
96              throw new IllegalArgumentException("Provided salt must be at least 16 bytes in size.");
97          }
98          salt = idSalt;
99      }
100 
101     /**
102      * Gets the data store used to manage stored IDs.
103      * 
104      * @return data store used to manage stored IDs
105      */
106     public StoredIDStore getStoredIDStore() {
107         return pidStore;
108     }
109 
110     /**
111      * Gets the salt used when computing the ID.
112      * 
113      * @return salt used when computing the ID
114      */
115     public byte[] getSalt() {
116         return salt;
117     }
118 
119     /**
120      * Gets the ID of the attribute whose first value is used when generating the computed ID.
121      * 
122      * @return ID of the attribute whose first value is used when generating the computed ID
123      */
124     public String getSourceAttributeId() {
125         return sourceAttribute;
126     }
127 
128     /**
129      * Gets the ID of the attribute generated by this connector.
130      * 
131      * @return ID of the attribute generated by this connector
132      */
133     public String getGeneratedAttributeId() {
134         return generatedAttribute;
135     }
136 
137     /** {@inheritDoc} */
138     public void validate() throws AttributeResolutionException {
139         if (getDependencyIds() == null || getDependencyIds().size() != 1) {
140             log.error("Stored ID " + getId() + " data connectore requires exactly one dependency");
141             throw new AttributeResolutionException("Computed ID " + getId()
142                     + " data connectore requires exactly one dependency");
143         }
144 
145         try {
146             pidStore.getActivePersistentIdEntry("1");
147         } catch (SQLException e) {
148             throw new AttributeResolutionException("Unable to connect to persistent ID store.");
149         }
150     }
151 
152     /** {@inheritDoc} */
153     public Map<String, BaseAttribute> resolve(ShibbolethResolutionContext resolutionContext)
154             throws AttributeResolutionException {
155         Map<String, BaseAttribute> attributes = new LazyMap<String, BaseAttribute>();
156 
157         String principalName = DatatypeHelper.safeTrimOrNullString(resolutionContext.getAttributeRequestContext()
158                 .getPrincipalName());
159 
160         String localId = getLocalId(resolutionContext);
161         if (localId == null) {
162             log.debug("No user local ID available, skipping ID creation.");
163             return attributes;
164         }
165 
166         String localEntityId = DatatypeHelper.safeTrimOrNullString(resolutionContext.getAttributeRequestContext()
167                 .getLocalEntityId());
168         if (localEntityId == null) {
169             log.debug("No local entity ID available, skipping ID creation.");
170             return attributes;
171         }
172 
173         String peerEntityId = getPeerEntityId(resolutionContext);
174         if (peerEntityId == null) {
175             log.debug("No peer entity ID available, skipping ID creation.");
176             return attributes;
177         }
178 
179         String persistentId = getStoredId(principalName, localEntityId, peerEntityId, localId);
180         if (persistentId != null) {
181             BasicAttribute<String> attribute = new BasicAttribute<String>();
182             attribute.setId(getGeneratedAttributeId());
183             attribute.getValues().add(persistentId);
184             attributes.put(attribute.getId(), attribute);
185         }
186         return attributes;
187     }
188 
189     /**
190      * Gets the persistent ID stored in the database. If one does not exist it is created.
191      * 
192      * @param principalName principal name of the user to whom the persistent ID belongs
193      * @param localEntityId ID of the local entity associated with the persistent ID
194      * @param peerEntityId ID of the peer entity associated with the persistent ID
195      * @param localId principal the the persistent ID represents
196      * 
197      * @return persistent ID
198      * 
199      * @throws AttributeResolutionException thrown if there is a problem retrieving or storing the persistent ID
200      */
201     protected String getStoredId(String principalName, String localEntityId, String peerEntityId, String localId)
202             throws AttributeResolutionException {
203         PersistentIdEntry idEntry;
204         try {
205             log.debug("Checking for existing, active, stored ID for principal '{}'", principalName);
206             idEntry = pidStore.getActivePersistentIdEntry(localEntityId, peerEntityId, localId);
207             if (idEntry == null) {
208                 log.debug("No existing, active, stored ID, creating a new one for principal '{}'", principalName);
209                 idEntry = createPersistentId(principalName, localEntityId, peerEntityId, localId);
210                 pidStore.storePersistentIdEntry(idEntry);
211                 log.debug("Created stored ID '{}'", idEntry);
212             } else {
213                 log.debug("Located existing stored ID {}", idEntry);
214             }
215 
216             return idEntry.getPersistentId();
217         } catch (SQLException e) {
218             log.debug("Database error retrieving persistent identifier", e);
219             throw new AttributeResolutionException("Database error retrieving persistent identifier", e);
220         }
221     }
222 
223     /**
224      * Gets the local ID component of the persistent ID.
225      * 
226      * @param resolutionContext current resolution context
227      * 
228      * @return local ID component of the persistent ID
229      * 
230      * @throws AttributeResolutionException thrown if there is a problem resolving the local id
231      */
232     protected String getLocalId(ShibbolethResolutionContext resolutionContext) throws AttributeResolutionException {
233         Collection<Object> sourceIdValues = getValuesFromAllDependencies(resolutionContext, getSourceAttributeId());
234         if (sourceIdValues == null || sourceIdValues.isEmpty()) {
235             log.debug("Source attribute {} for connector {} provide no values.  No identifier will be generated.",
236                     getSourceAttributeId(), getId());
237             return null;
238         }
239 
240         if (sourceIdValues.size() > 1) {
241             log.warn("Source attribute {} for connector {} has more than one value, only the first value is used",
242                     getSourceAttributeId(), getId());
243         }
244 
245         return sourceIdValues.iterator().next().toString();
246     }
247 
248     /**
249      * Gets the entity ID used for the peer. If the inbound request is a SAML 2 authentication context and contains a
250      * NameIDPolicy than the SPNameQualifier is used if present, otherwise the inbound message issuer is used.
251      * 
252      * @param resolutionContext current attribute resolution context
253      * 
254      * @return the entity ID to use for the peer
255      */
256     protected String getPeerEntityId(ShibbolethResolutionContext resolutionContext) {
257         String peerEntityId = null;
258 
259         log.debug("Determining if peer entity ID will be the SPNameQualifier from a SAML 2 authentication statement");
260         XMLObject inboundMessage = resolutionContext.getAttributeRequestContext().getInboundMessage();
261         if (inboundMessage instanceof AuthnRequest) {
262             AuthnRequest authnRequest = (AuthnRequest) inboundMessage;
263             if (authnRequest.getNameIDPolicy() != null) {
264                 peerEntityId = DatatypeHelper.safeTrimOrNullString(authnRequest.getNameIDPolicy().getSPNameQualifier());
265                 if (peerEntityId == null) {
266                     log
267                             .debug("SAML 2 authentication request did not contain an SPNameQualifier within its NameIDPolicy");
268                 } else {
269                     log
270                             .debug("SAML 2 authentication request contained an SPNameQualifier, within its NameIDPolicy.  Using that as peer entity ID");
271                 }
272             } else {
273                 log.debug("SAML 2 authentication request did not contain a NameIDPolicy");
274             }
275         } else {
276             log.debug("Inbound message was not a SAML 2 authentication request");
277         }
278 
279         if (peerEntityId == null) {
280             log.debug("Determining if inbound message issuer is available for use as peer entity ID");
281             peerEntityId = resolutionContext.getAttributeRequestContext().getInboundMessageIssuer();
282         }
283 
284         return peerEntityId;
285     }
286 
287     /**
288      * Creates a persistent ID that is unique for a given local/peer/localId tuple.
289      * 
290      * If an ID has never been issued for to the given tuple then an ID is created by taking a SHA-1 hash of the peer's
291      * entity ID, the local ID, and a salt. This is to ensure compatability with IDs created by the now deprecated
292      * {@link ComputedIDDataConnector}.
293      * 
294      * If an ID has been issued to the given tuple than a new, random type 4 UUID is generated as the persistent ID.
295      * 
296      * @param principalName principal name of the user to whom the persistent ID belongs
297      * @param localEntityId ID of the local entity associated with the persistent ID
298      * @param peerEntityId ID of the peer entity associated with the persistent ID
299      * @param localId principal the the persistent ID represents
300      * 
301      * @return the created identifier
302      * 
303      * @throws SQLException thrown if there is a problem communication with the database
304      */
305     protected PersistentIdEntry createPersistentId(String principalName, String localEntityId, String peerEntityId,
306             String localId) throws SQLException {
307         PersistentIdEntry entry = pidStore.new PersistentIdEntry();
308         entry.setLocalEntityId(localEntityId);
309         entry.setPeerEntityId(peerEntityId);
310         entry.setPrincipalName(principalName);
311         entry.setLocalId(localId);
312 
313         String persistentId;
314         int numberOfExistingEntries = pidStore.getNumberOfPersistentIdEntries(entry.getLocalEntityId(), entry
315                 .getPeerEntityId(), entry.getLocalId());
316 
317         if (numberOfExistingEntries == 0) {
318             try {
319                 MessageDigest md = MessageDigest.getInstance("SHA");
320                 md.update(entry.getPeerEntityId().getBytes());
321                 md.update((byte) '!');
322                 md.update(localId.getBytes());
323                 md.update((byte) '!');
324                 persistentId = Base64.encodeBytes(md.digest(salt));
325             } catch (NoSuchAlgorithmException e) {
326                 log.error("JVM error, SHA-1 is not supported, unable to compute ID");
327                 throw new SQLException("SHA-1 is not supported, unable to compute ID");
328             }
329         } else {
330             persistentId = UUID.randomUUID().toString();
331         }
332 
333         while (pidStore.getPersistentIdEntry(persistentId, false) != null) {
334             log.debug("Generated persistent ID was already assigned to another user, regenerating");
335             persistentId = UUID.randomUUID().toString();
336         }
337 
338         entry.setPersistentId(persistentId);
339 
340         entry.setCreationTime(new Timestamp(System.currentTimeMillis()));
341 
342         return entry;
343     }
344 }