1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
49
50
51
52
53
54
55
56 public class StoredIDDataConnector extends BaseDataConnector {
57
58
59 private final Logger log = LoggerFactory.getLogger(StoredIDDataConnector.class);
60
61
62 private StoredIDStore pidStore;
63
64
65 private String generatedAttribute;
66
67
68 private String sourceAttribute;
69
70
71 private byte[] salt;
72
73
74
75
76
77
78
79
80
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
107
108
109
110 public StoredIDStore getStoredIDStore() {
111 return pidStore;
112 }
113
114
115
116
117
118
119 public byte[] getSalt() {
120 return salt;
121 }
122
123
124
125
126
127
128 public String getSourceAttributeId() {
129 return sourceAttribute;
130 }
131
132
133
134
135
136
137 public String getGeneratedAttributeId() {
138 return generatedAttribute;
139 }
140
141
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
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
195
196
197
198
199
200
201
202
203
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
229
230
231
232
233
234
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
254
255
256
257
258
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
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
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 }