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