001 /*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License"). You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at
010 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
011 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
012 * See the License for the specific language governing permissions
013 * and limitations under the License.
014 *
015 * When distributing Covered Code, include this CDDL HEADER in each
016 * file and include the License file at
017 * trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable,
018 * add the following below this CDDL HEADER, with the fields enclosed
019 * by brackets "[]" replaced with your own identifying information:
020 * Portions Copyright [yyyy] [name of copyright owner]
021 *
022 * CDDL HEADER END
023 *
024 *
025 * Copyright 2007-2008 Sun Microsystems, Inc.
026 */
027 package org.opends.server.extensions;
028
029
030
031 import java.security.cert.Certificate;
032 import java.security.cert.X509Certificate;
033 import javax.security.auth.x500.X500Principal;
034 import java.util.ArrayList;
035 import java.util.Collection;
036 import java.util.LinkedHashMap;
037 import java.util.LinkedList;
038 import java.util.List;
039 import java.util.Set;
040
041 import org.opends.messages.Message;
042 import org.opends.server.admin.server.ConfigurationChangeListener;
043 import org.opends.server.admin.std.server.CertificateMapperCfg;
044 import org.opends.server.admin.std.server.
045 SubjectAttributeToUserAttributeCertificateMapperCfg;
046 import org.opends.server.api.Backend;
047 import org.opends.server.api.CertificateMapper;
048 import org.opends.server.config.ConfigException;
049 import org.opends.server.core.DirectoryServer;
050 import org.opends.server.loggers.ErrorLogger;
051 import org.opends.server.loggers.debug.DebugTracer;
052 import org.opends.server.protocols.internal.InternalClientConnection;
053 import org.opends.server.protocols.internal.InternalSearchOperation;
054 import org.opends.server.types.DirectoryException;
055 import org.opends.server.types.AttributeType;
056 import org.opends.server.types.ConfigChangeResult;
057 import org.opends.server.types.DebugLogLevel;
058 import org.opends.server.types.DN;
059 import org.opends.server.types.Entry;
060 import org.opends.server.types.IndexType;
061 import org.opends.server.types.InitializationException;
062 import org.opends.server.types.RDN;
063 import org.opends.server.types.ResultCode;
064 import org.opends.server.types.SearchFilter;
065 import org.opends.server.types.SearchResultEntry;
066 import org.opends.server.types.SearchScope;
067
068 import static org.opends.messages.ExtensionMessages.*;
069 import static org.opends.server.loggers.debug.DebugLogger.*;
070 import static org.opends.server.util.StaticUtils.*;
071
072
073
074 /**
075 * This class implements a very simple Directory Server certificate mapper that
076 * will map a certificate to a user based on attributes contained in both the
077 * certificate subject and the user's entry. The configuration may include
078 * mappings from certificate attributes to attributes in user entries, and all
079 * of those certificate attributes that are present in the subject will be used
080 * to search for matching user entries.
081 */
082 public class SubjectAttributeToUserAttributeCertificateMapper
083 extends CertificateMapper<
084 SubjectAttributeToUserAttributeCertificateMapperCfg>
085 implements ConfigurationChangeListener<
086 SubjectAttributeToUserAttributeCertificateMapperCfg>
087 {
088 /**
089 * The tracer object for the debug logger.
090 */
091 private static final DebugTracer TRACER = getTracer();
092
093 // The DN of the configuration entry for this certificate mapper.
094 private DN configEntryDN;
095
096 // The mappings between certificate attribute names and user attribute types.
097 private LinkedHashMap<String,AttributeType> attributeMap;
098
099 // The current configuration for this certificate mapper.
100 private SubjectAttributeToUserAttributeCertificateMapperCfg currentConfig;
101
102
103
104 /**
105 * Creates a new instance of this certificate mapper. Note that all actual
106 * initialization should be done in the
107 * <CODE>initializeCertificateMapper</CODE> method.
108 */
109 public SubjectAttributeToUserAttributeCertificateMapper()
110 {
111 super();
112 }
113
114
115
116 /**
117 * {@inheritDoc}
118 */
119 public void initializeCertificateMapper(
120 SubjectAttributeToUserAttributeCertificateMapperCfg
121 configuration)
122 throws ConfigException, InitializationException
123 {
124 configuration
125 .addSubjectAttributeToUserAttributeChangeListener(this);
126
127 currentConfig = configuration;
128 configEntryDN = configuration.dn();
129
130 // Get and validate the subject attribute to user attribute mappings.
131 attributeMap = new LinkedHashMap<String,AttributeType>();
132 for (String mapStr : configuration.getSubjectAttributeMapping())
133 {
134 String lowerMap = toLowerCase(mapStr);
135 int colonPos = lowerMap.indexOf(':');
136 if (colonPos <= 0)
137 {
138 Message message = ERR_SATUACM_INVALID_MAP_FORMAT.get(
139 String.valueOf(configEntryDN), mapStr);
140 throw new ConfigException(message);
141 }
142
143 String certAttrName = lowerMap.substring(0, colonPos).trim();
144 String userAttrName = lowerMap.substring(colonPos+1).trim();
145 if ((certAttrName.length() == 0) || (userAttrName.length() == 0))
146 {
147 Message message = ERR_SATUACM_INVALID_MAP_FORMAT.get(
148 String.valueOf(configEntryDN), mapStr);
149 throw new ConfigException(message);
150 }
151
152 if (attributeMap.containsKey(certAttrName))
153 {
154 Message message = ERR_SATUACM_DUPLICATE_CERT_ATTR.get(
155 String.valueOf(configEntryDN), certAttrName);
156 throw new ConfigException(message);
157 }
158
159 AttributeType userAttrType =
160 DirectoryServer.getAttributeType(userAttrName, false);
161 if (userAttrType == null)
162 {
163 Message message = ERR_SATUACM_NO_SUCH_ATTR.get(
164 mapStr, String.valueOf(configEntryDN), userAttrName);
165 throw new ConfigException(message);
166 }
167
168 for (AttributeType attrType : attributeMap.values())
169 {
170 if (attrType.equals(userAttrType))
171 {
172 Message message = ERR_SATUACM_DUPLICATE_USER_ATTR.get(
173 String.valueOf(configEntryDN), attrType.getNameOrOID());
174 throw new ConfigException(message);
175 }
176 }
177
178 attributeMap.put(certAttrName, userAttrType);
179 }
180
181 // Make sure that all the user attributes are configured with equality
182 // indexes in all appropriate backends.
183 Set<DN> cfgBaseDNs = configuration.getUserBaseDN();
184 if ((cfgBaseDNs == null) || cfgBaseDNs.isEmpty())
185 {
186 cfgBaseDNs = DirectoryServer.getPublicNamingContexts().keySet();
187 }
188
189 for (DN baseDN : cfgBaseDNs)
190 {
191 for (AttributeType t : attributeMap.values())
192 {
193 Backend b = DirectoryServer.getBackend(baseDN);
194 if ((b != null) && (! b.isIndexed(t, IndexType.EQUALITY)))
195 {
196 Message message = WARN_SATUACM_ATTR_UNINDEXED.get(
197 configuration.dn().toString(),
198 t.getNameOrOID(), b.getBackendID());
199 ErrorLogger.logError(message);
200 }
201 }
202 }
203 }
204
205
206
207 /**
208 * {@inheritDoc}
209 */
210 public void finalizeCertificateMapper()
211 {
212 currentConfig
213 .removeSubjectAttributeToUserAttributeChangeListener(this);
214 }
215
216
217
218 /**
219 * {@inheritDoc}
220 */
221 public Entry mapCertificateToUser(Certificate[] certificateChain)
222 throws DirectoryException
223 {
224 SubjectAttributeToUserAttributeCertificateMapperCfg config =
225 currentConfig;
226 LinkedHashMap<String,AttributeType> attributeMap = this.attributeMap;
227
228
229 // Make sure that a peer certificate was provided.
230 if ((certificateChain == null) || (certificateChain.length == 0))
231 {
232 Message message = ERR_SATUACM_NO_PEER_CERTIFICATE.get();
233 throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message);
234 }
235
236
237 // Get the first certificate in the chain. It must be an X.509 certificate.
238 X509Certificate peerCertificate;
239 try
240 {
241 peerCertificate = (X509Certificate) certificateChain[0];
242 }
243 catch (Exception e)
244 {
245 if (debugEnabled())
246 {
247 TRACER.debugCaught(DebugLogLevel.ERROR, e);
248 }
249
250 Message message = ERR_SATUACM_PEER_CERT_NOT_X509.get(
251 String.valueOf(certificateChain[0].getType()));
252 throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message);
253 }
254
255
256 // Get the subject from the peer certificate and use it to create a search
257 // filter.
258 DN peerDN;
259 X500Principal peerPrincipal = peerCertificate.getSubjectX500Principal();
260 String peerName = peerPrincipal.getName(X500Principal.RFC2253);
261 try
262 {
263 peerDN = DN.decode(peerName);
264 }
265 catch (DirectoryException de)
266 {
267 Message message = ERR_SATUACM_CANNOT_DECODE_SUBJECT_AS_DN.get(
268 peerName, de.getMessageObject());
269 throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message,
270 de);
271 }
272
273 LinkedList<SearchFilter> filterComps = new LinkedList<SearchFilter>();
274 for (int i=0; i < peerDN.getNumComponents(); i++)
275 {
276 RDN rdn = peerDN.getRDN(i);
277 for (int j=0; j < rdn.getNumValues(); j++)
278 {
279 String lowerName = toLowerCase(rdn.getAttributeName(j));
280 AttributeType attrType = attributeMap.get(lowerName);
281 if (attrType != null)
282 {
283 filterComps.add(SearchFilter.createEqualityFilter(attrType,
284 rdn.getAttributeValue(j)));
285 }
286 }
287 }
288
289 if (filterComps.isEmpty())
290 {
291 Message message = ERR_SATUACM_NO_MAPPABLE_ATTRIBUTES.get(peerName);
292 throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message);
293 }
294
295 SearchFilter filter = SearchFilter.createANDFilter(filterComps);
296
297
298 // If we have an explicit set of base DNs, then use it. Otherwise, use the
299 // set of public naming contexts in the server.
300 Collection<DN> baseDNs = config.getUserBaseDN();
301 if ((baseDNs == null) || baseDNs.isEmpty())
302 {
303 baseDNs = DirectoryServer.getPublicNamingContexts().keySet();
304 }
305
306
307 // For each base DN, issue an internal search in an attempt to map the
308 // certificate.
309 Entry userEntry = null;
310 InternalClientConnection conn =
311 InternalClientConnection.getRootConnection();
312 for (DN baseDN : baseDNs)
313 {
314 InternalSearchOperation searchOperation =
315 conn.processSearch(baseDN, SearchScope.WHOLE_SUBTREE, filter);
316 for (SearchResultEntry entry : searchOperation.getSearchEntries())
317 {
318 if (userEntry == null)
319 {
320 userEntry = entry;
321 }
322 else
323 {
324 Message message = ERR_SATUACM_MULTIPLE_MATCHING_ENTRIES.
325 get(peerName, String.valueOf(userEntry.getDN()),
326 String.valueOf(entry.getDN()));
327 throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message);
328 }
329 }
330 }
331
332
333 // If we've gotten here, then we either found exactly one user entry or we
334 // didn't find any. Either way, return the entry or null to the caller.
335 return userEntry;
336 }
337
338
339
340 /**
341 * {@inheritDoc}
342 */
343 @Override()
344 public boolean isConfigurationAcceptable(CertificateMapperCfg configuration,
345 List<Message> unacceptableReasons)
346 {
347 SubjectAttributeToUserAttributeCertificateMapperCfg config =
348 (SubjectAttributeToUserAttributeCertificateMapperCfg) configuration;
349 return isConfigurationChangeAcceptable(config, unacceptableReasons);
350 }
351
352
353
354 /**
355 * {@inheritDoc}
356 */
357 public boolean isConfigurationChangeAcceptable(
358 SubjectAttributeToUserAttributeCertificateMapperCfg
359 configuration,
360 List<Message> unacceptableReasons)
361 {
362 boolean configAcceptable = true;
363 DN cfgEntryDN = configuration.dn();
364
365 // Get and validate the subject attribute to user attribute mappings.
366 LinkedHashMap<String,AttributeType> newAttributeMap =
367 new LinkedHashMap<String,AttributeType>();
368 mapLoop:
369 for (String mapStr : configuration.getSubjectAttributeMapping())
370 {
371 String lowerMap = toLowerCase(mapStr);
372 int colonPos = lowerMap.indexOf(':');
373 if (colonPos <= 0)
374 {
375 unacceptableReasons.add(ERR_SATUACM_INVALID_MAP_FORMAT.get(
376 String.valueOf(cfgEntryDN),
377 mapStr));
378 configAcceptable = false;
379 break;
380 }
381
382 String certAttrName = lowerMap.substring(0, colonPos).trim();
383 String userAttrName = lowerMap.substring(colonPos+1).trim();
384 if ((certAttrName.length() == 0) || (userAttrName.length() == 0))
385 {
386 unacceptableReasons.add(ERR_SATUACM_INVALID_MAP_FORMAT.get(
387 String.valueOf(cfgEntryDN),
388 mapStr));
389 configAcceptable = false;
390 break;
391 }
392
393 if (newAttributeMap.containsKey(certAttrName))
394 {
395 unacceptableReasons.add(ERR_SATUACM_DUPLICATE_CERT_ATTR.get(
396 String.valueOf(cfgEntryDN),
397 certAttrName));
398 configAcceptable = false;
399 break;
400 }
401
402 AttributeType userAttrType =
403 DirectoryServer.getAttributeType(userAttrName, false);
404 if (userAttrType == null)
405 {
406 unacceptableReasons.add(ERR_SATUACM_NO_SUCH_ATTR.get(
407 mapStr,
408 String.valueOf(cfgEntryDN),
409 userAttrName));
410 configAcceptable = false;
411 break;
412 }
413
414 for (AttributeType attrType : newAttributeMap.values())
415 {
416 if (attrType.equals(userAttrType))
417 {
418 unacceptableReasons.add(ERR_SATUACM_DUPLICATE_USER_ATTR.get(
419 String.valueOf(cfgEntryDN),
420 attrType.getNameOrOID()));
421 configAcceptable = false;
422 break mapLoop;
423 }
424 }
425
426 newAttributeMap.put(certAttrName, userAttrType);
427 }
428
429 return configAcceptable;
430 }
431
432
433
434 /**
435 * {@inheritDoc}
436 */
437 public ConfigChangeResult applyConfigurationChange(
438 SubjectAttributeToUserAttributeCertificateMapperCfg
439 configuration)
440 {
441 ResultCode resultCode = ResultCode.SUCCESS;
442 boolean adminActionRequired = false;
443 ArrayList<Message> messages = new ArrayList<Message>();
444
445
446 // Get and validate the subject attribute to user attribute mappings.
447 LinkedHashMap<String,AttributeType> newAttributeMap =
448 new LinkedHashMap<String,AttributeType>();
449 mapLoop:
450 for (String mapStr : configuration.getSubjectAttributeMapping())
451 {
452 String lowerMap = toLowerCase(mapStr);
453 int colonPos = lowerMap.indexOf(':');
454 if (colonPos <= 0)
455 {
456 if (resultCode == ResultCode.SUCCESS)
457 {
458 resultCode = ResultCode.CONSTRAINT_VIOLATION;
459 }
460
461
462 messages.add(ERR_SATUACM_INVALID_MAP_FORMAT.get(
463 String.valueOf(configEntryDN), mapStr));
464 break;
465 }
466
467 String certAttrName = lowerMap.substring(0, colonPos).trim();
468 String userAttrName = lowerMap.substring(colonPos+1).trim();
469 if ((certAttrName.length() == 0) || (userAttrName.length() == 0))
470 {
471 if (resultCode == ResultCode.SUCCESS)
472 {
473 resultCode = ResultCode.CONSTRAINT_VIOLATION;
474 }
475
476
477 messages.add(ERR_SATUACM_INVALID_MAP_FORMAT.get(
478 String.valueOf(configEntryDN), mapStr));
479 break;
480 }
481
482 if (newAttributeMap.containsKey(certAttrName))
483 {
484 if (resultCode == ResultCode.SUCCESS)
485 {
486 resultCode = ResultCode.CONSTRAINT_VIOLATION;
487 }
488
489
490 messages.add(ERR_SATUACM_DUPLICATE_CERT_ATTR.get(
491 String.valueOf(configEntryDN),
492 certAttrName));
493 break;
494 }
495
496 AttributeType userAttrType =
497 DirectoryServer.getAttributeType(userAttrName, false);
498 if (userAttrType == null)
499 {
500 if (resultCode == ResultCode.SUCCESS)
501 {
502 resultCode = ResultCode.CONSTRAINT_VIOLATION;
503 }
504
505
506 messages.add(ERR_SATUACM_NO_SUCH_ATTR.get(
507 mapStr, String.valueOf(configEntryDN),
508 userAttrName));
509 break;
510 }
511
512 for (AttributeType attrType : newAttributeMap.values())
513 {
514 if (attrType.equals(userAttrType))
515 {
516 if (resultCode == ResultCode.SUCCESS)
517 {
518 resultCode = ResultCode.CONSTRAINT_VIOLATION;
519 }
520
521
522 messages.add(ERR_SATUACM_DUPLICATE_USER_ATTR.get(
523 String.valueOf(configEntryDN),
524 attrType.getNameOrOID()));
525 break mapLoop;
526 }
527 }
528
529 newAttributeMap.put(certAttrName, userAttrType);
530 }
531
532 // Make sure that all the user attributes are configured with equality
533 // indexes in all appropriate backends.
534 Set<DN> cfgBaseDNs = configuration.getUserBaseDN();
535 if ((cfgBaseDNs == null) || cfgBaseDNs.isEmpty())
536 {
537 cfgBaseDNs = DirectoryServer.getPublicNamingContexts().keySet();
538 }
539
540 for (DN baseDN : cfgBaseDNs)
541 {
542 for (AttributeType t : newAttributeMap.values())
543 {
544 Backend b = DirectoryServer.getBackend(baseDN);
545 if ((b != null) && (! b.isIndexed(t, IndexType.EQUALITY)))
546 {
547 Message message = WARN_SATUACM_ATTR_UNINDEXED.get(
548 configuration.dn().toString(),
549 t.getNameOrOID(), b.getBackendID());
550 messages.add(message);
551 ErrorLogger.logError(message);
552 }
553 }
554 }
555
556 if (resultCode == ResultCode.SUCCESS)
557 {
558 attributeMap = newAttributeMap;
559 currentConfig = configuration;
560 }
561
562
563 return new ConfigChangeResult(resultCode, adminActionRequired, messages);
564 }
565 }
566