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