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.HashMap;
25  import java.util.Map;
26  import java.util.UUID;
27  
28  import javax.sql.DataSource;
29  
30  import org.opensaml.xml.util.Base64;
31  import org.opensaml.xml.util.DatatypeHelper;
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         String persistentId = getStoredId(resolutionContext);
138         BasicAttribute<String> attribute = new BasicAttribute<String>();
139         attribute.setId(getGeneratedAttributeId());
140         attribute.getValues().add(persistentId);
141 
142         Map<String, BaseAttribute> attributes = new HashMap<String, BaseAttribute>();
143         attributes.put(attribute.getId(), attribute);
144         return attributes;
145     }
146 
147     /** {@inheritDoc} */
148     public void validate() throws AttributeResolutionException {
149         if (getDependencyIds() == null || getDependencyIds().size() != 1) {
150             log.error("Computed ID " + getId() + " data connectore requires exactly one dependency");
151             throw new AttributeResolutionException("Computed ID " + getId()
152                     + " data connectore requires exactly one dependency");
153         }
154     }
155 
156     /**
157      * Gets the persistent ID stored in the database. If one does not exist it is created.
158      * 
159      * @param resolutionContext current resolution context
160      * 
161      * @return persistent ID
162      * 
163      * @throws AttributeResolutionException thrown if there is a problem retrieving or storing the persistent ID
164      */
165     protected String getStoredId(ShibbolethResolutionContext resolutionContext) throws AttributeResolutionException {
166         SAMLProfileRequestContext requestCtx = resolutionContext.getAttributeRequestContext();
167 
168         String localId = getLocalId(resolutionContext);
169         PersistentIdEntry idEntry;
170         try {
171             idEntry = pidStore.getActivePersistentIdEntry(requestCtx.getLocalEntityId(), requestCtx
172                     .getInboundMessageIssuer(), localId);
173             if (idEntry == null) {
174                 idEntry = createPersistentId(resolutionContext, localId, salt);
175                 pidStore.storePersistentIdEntry(idEntry);
176                 log.debug("Created stored ID {}", idEntry);
177             }else{
178                 log.debug("Located existing stored ID {}", idEntry);
179             }
180 
181             return idEntry.getPersistentId();
182         } catch (SQLException e) {
183             log.error("Database error retrieving persistent identifier", e);
184             throw new AttributeResolutionException("Database error retrieving persistent identifier", e);
185         }
186     }
187 
188     /**
189      * Gets the local ID component of the persistent ID.
190      * 
191      * @param resolutionContext current resolution context
192      * 
193      * @return local ID component of the persistent ID
194      * 
195      * @throws AttributeResolutionException thrown if there is a problem resolving the local id
196      */
197     protected String getLocalId(ShibbolethResolutionContext resolutionContext) throws AttributeResolutionException {
198         Collection<Object> sourceIdValues = getValuesFromAllDependencies(resolutionContext, getSourceAttributeId());
199         if (sourceIdValues == null || sourceIdValues.isEmpty()) {
200             log.error("Source attribute {} for connector {} provide no values", getSourceAttributeId(), getId());
201             throw new AttributeResolutionException("Source attribute " + getSourceAttributeId() + " for connector "
202                     + getId() + " provided no values");
203         }
204 
205         if (sourceIdValues.size() > 1) {
206             log.warn("Source attribute {} for connector {} has more than one value, only the first value is used",
207                     getSourceAttributeId(), getId());
208         }
209 
210         return sourceIdValues.iterator().next().toString();
211     }
212 
213     /**
214      * Creates a persistent ID that is unique for a given local/peer/localId tuple.
215      * 
216      * 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
217      * entity ID, the local ID, and a salt. This is to ensure compatability with IDs created by the now deprecated
218      * {@link ComputedIDDataConnector}.
219      * 
220      * If an ID has been issued to the given tuple than a new, random type 4 UUID is generated as the persistent ID.
221      * 
222      * @param resolutionContext current resolution context
223      * @param localId principal the the persistent ID represents
224      * @param salt salt used when computing a persistent ID via SHA-1 hash
225      * 
226      * @return the created identifier
227      * 
228      * @throws SQLException thrown if there is a problem communication with the database
229      */
230     protected PersistentIdEntry createPersistentId(ShibbolethResolutionContext resolutionContext, String localId,
231             byte[] salt) throws SQLException {
232         PersistentIdEntry entry = pidStore.new PersistentIdEntry();
233         entry.setLocalEntityId(resolutionContext.getAttributeRequestContext().getLocalEntityId());
234         entry.setPeerEntityId(resolutionContext.getAttributeRequestContext().getInboundMessageIssuer());
235         entry.setPrincipalName(resolutionContext.getAttributeRequestContext().getPrincipalName());
236         entry.setLocalId(localId);
237 
238         String persisentId;
239         int numberOfExistingEntries = pidStore.getNumberOfPersistentIdEntries(entry.getLocalEntityId(), entry
240                 .getPeerEntityId(), entry.getLocalId());
241         if (numberOfExistingEntries == 0) {
242             try {
243                 MessageDigest md = MessageDigest.getInstance("SHA");
244                 md.update(entry.getPeerEntityId().getBytes());
245                 md.update((byte) '!');
246                 md.update(localId.getBytes());
247                 md.update((byte) '!');
248                 persisentId = Base64.encodeBytes(md.digest(salt));
249             } catch (NoSuchAlgorithmException e) {
250                 log.error("JVM error, SHA-1 is not supported, unable to compute ID");
251                 throw new SQLException("SHA-1 is not supported, unable to compute ID");
252             }
253         } else {
254             persisentId = UUID.randomUUID().toString();
255         }
256         entry.setPersistentId(persisentId);
257 
258         entry.setCreationTime(new Timestamp(System.currentTimeMillis()));
259 
260         return entry;
261     }
262 
263 }