View Javadoc

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