001/* 002 * Copyright 2016-2017 UnboundID Corp. 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2016-2017 UnboundID Corp. 007 * 008 * This program is free software; you can redistribute it and/or modify 009 * it under the terms of the GNU General Public License (GPLv2 only) 010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 011 * as published by the Free Software Foundation. 012 * 013 * This program is distributed in the hope that it will be useful, 014 * but WITHOUT ANY WARRANTY; without even the implied warranty of 015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 016 * GNU General Public License for more details. 017 * 018 * You should have received a copy of the GNU General Public License 019 * along with this program; if not, see <http://www.gnu.org/licenses>. 020 */ 021package com.unboundid.ldap.sdk.transformations; 022 023 024 025import java.util.ArrayList; 026import java.util.Collection; 027import java.util.Collections; 028import java.util.HashSet; 029import java.util.Set; 030 031import com.unboundid.asn1.ASN1OctetString; 032import com.unboundid.ldap.matchingrules.DistinguishedNameMatchingRule; 033import com.unboundid.ldap.matchingrules.MatchingRule; 034import com.unboundid.ldap.sdk.Attribute; 035import com.unboundid.ldap.sdk.DN; 036import com.unboundid.ldap.sdk.Entry; 037import com.unboundid.ldap.sdk.Modification; 038import com.unboundid.ldap.sdk.RDN; 039import com.unboundid.ldap.sdk.schema.AttributeTypeDefinition; 040import com.unboundid.ldap.sdk.schema.Schema; 041import com.unboundid.ldif.LDIFAddChangeRecord; 042import com.unboundid.ldif.LDIFChangeRecord; 043import com.unboundid.ldif.LDIFDeleteChangeRecord; 044import com.unboundid.ldif.LDIFModifyChangeRecord; 045import com.unboundid.ldif.LDIFModifyDNChangeRecord; 046import com.unboundid.util.Debug; 047import com.unboundid.util.StaticUtils; 048import com.unboundid.util.ThreadSafety; 049import com.unboundid.util.ThreadSafetyLevel; 050 051 052 053/** 054 * This class provides an implementation of an entry and LDIF change record 055 * transformation that will redact the values of a specified set of attributes 056 * so that it will be possible to determine whether the attribute had been 057 * present in an entry or change record, but not what the values were for that 058 * attribute. 059 */ 060@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 061public final class RedactAttributeTransformation 062 implements EntryTransformation, LDIFChangeRecordTransformation 063{ 064 // Indicates whether to preserve the number of values in redacted attributes. 065 private final boolean preserveValueCount; 066 067 // Indicates whether to redact 068 private final boolean redactDNAttributes; 069 070 // The schema to use when processing. 071 private final Schema schema; 072 073 // The set of attributes to strip from entries. 074 private final Set<String> attributes; 075 076 077 078 /** 079 * Creates a new redact attribute transformation that will redact the values 080 * of the specified attributes. 081 * 082 * @param schema The schema to use to identify alternate names 083 * that may be used to reference the attributes to 084 * redact. It may be {@code null} to use a 085 * default standard schema. 086 * @param redactDNAttributes Indicates whether to redact values of the 087 * target attributes that appear in DNs. This 088 * includes the DNs of the entries to process as 089 * well as the values of attributes with a DN 090 * syntax. 091 * @param preserveValueCount Indicates whether to preserve the number of 092 * values in redacted attributes. If this is 093 * {@code true}, then multivalued attributes that 094 * are redacted will have the same number of 095 * values but each value will be replaced with 096 * "***REDACTED{num}***" where "{num}" is a 097 * counter that increments for each value. If 098 * this is {@code false}, then the set of values 099 * will always be replaced with a single value of 100 * "***REDACTED***" regardless of whether the 101 * original attribute had one or multiple values. 102 * @param attributes The names of the attributes whose values should 103 * be redacted. It must must not be {@code null} 104 * or empty. 105 */ 106 public RedactAttributeTransformation(final Schema schema, 107 final boolean redactDNAttributes, 108 final boolean preserveValueCount, 109 final String... attributes) 110 { 111 this(schema, redactDNAttributes, preserveValueCount, 112 StaticUtils.toList(attributes)); 113 } 114 115 116 117 /** 118 * Creates a new redact attribute transformation that will redact the values 119 * of the specified attributes. 120 * 121 * @param schema The schema to use to identify alternate names 122 * that may be used to reference the attributes to 123 * redact. It may be {@code null} to use a 124 * default standard schema. 125 * @param redactDNAttributes Indicates whether to redact values of the 126 * target attributes that appear in DNs. This 127 * includes the DNs of the entries to process as 128 * well as the values of attributes with a DN 129 * syntax. 130 * @param preserveValueCount Indicates whether to preserve the number of 131 * values in redacted attributes. If this is 132 * {@code true}, then multivalued attributes that 133 * are redacted will have the same number of 134 * values but each value will be replaced with 135 * "***REDACTED{num}***" where "{num}" is a 136 * counter that increments for each value. If 137 * this is {@code false}, then the set of values 138 * will always be replaced with a single value of 139 * "***REDACTED***" regardless of whether the 140 * original attribute had one or multiple values. 141 * @param attributes The names of the attributes whose values should 142 * be redacted. It must must not be {@code null} 143 * or empty. 144 */ 145 public RedactAttributeTransformation(final Schema schema, 146 final boolean redactDNAttributes, 147 final boolean preserveValueCount, 148 final Collection<String> attributes) 149 { 150 this.redactDNAttributes = redactDNAttributes; 151 this.preserveValueCount = preserveValueCount; 152 153 // If a schema was provided, then use it. Otherwise, use the default 154 // standard schema. 155 Schema s = schema; 156 if (s == null) 157 { 158 try 159 { 160 s = Schema.getDefaultStandardSchema(); 161 } 162 catch (final Exception e) 163 { 164 // This should never happen. 165 Debug.debugException(e); 166 } 167 } 168 this.schema = s; 169 170 171 // Identify all of the names that may be used to reference the attributes 172 // to redact. 173 final HashSet<String> attrNames = new HashSet<String>(3*attributes.size()); 174 for (final String attrName : attributes) 175 { 176 final String baseName = 177 Attribute.getBaseName(StaticUtils.toLowerCase(attrName)); 178 attrNames.add(baseName); 179 180 if (s != null) 181 { 182 final AttributeTypeDefinition at = s.getAttributeType(baseName); 183 if (at != null) 184 { 185 attrNames.add(StaticUtils.toLowerCase(at.getOID())); 186 for (final String name : at.getNames()) 187 { 188 attrNames.add(StaticUtils.toLowerCase(name)); 189 } 190 } 191 } 192 } 193 this.attributes = Collections.unmodifiableSet(attrNames); 194 } 195 196 197 198 /** 199 * {@inheritDoc} 200 */ 201 public Entry transformEntry(final Entry e) 202 { 203 if (e == null) 204 { 205 return null; 206 } 207 208 209 // If we should process entry DNs, then see if the DN contains any of the 210 // target attributes. 211 final String newDN; 212 if (redactDNAttributes) 213 { 214 newDN = redactDN(e.getDN()); 215 } 216 else 217 { 218 newDN = e.getDN(); 219 } 220 221 222 // Create a copy of the entry with all appropriate attributes redacted. 223 final Collection<Attribute> originalAttributes = e.getAttributes(); 224 final ArrayList<Attribute> newAttributes = 225 new ArrayList<Attribute>(originalAttributes.size()); 226 for (final Attribute a : originalAttributes) 227 { 228 final String baseName = StaticUtils.toLowerCase(a.getBaseName()); 229 if (attributes.contains(baseName)) 230 { 231 if (preserveValueCount && (a.size() > 1)) 232 { 233 final ASN1OctetString[] values = new ASN1OctetString[a.size()]; 234 for (int i=0; i < values.length; i++) 235 { 236 values[i] = new ASN1OctetString("***REDACTED" + (i+1) + "***"); 237 } 238 newAttributes.add(new Attribute(a.getName(), values)); 239 } 240 else 241 { 242 newAttributes.add(new Attribute(a.getName(), "***REDACTED***")); 243 } 244 } 245 else if (redactDNAttributes && (schema != null) && 246 (MatchingRule.selectEqualityMatchingRule(baseName, schema) 247 instanceof DistinguishedNameMatchingRule)) 248 { 249 250 final String[] originalValues = a.getValues(); 251 final String[] newValues = new String[originalValues.length]; 252 for (int i=0; i < originalValues.length; i++) 253 { 254 newValues[i] = redactDN(originalValues[i]); 255 } 256 newAttributes.add(new Attribute(a.getName(), schema, newValues)); 257 } 258 else 259 { 260 newAttributes.add(a); 261 } 262 } 263 264 return new Entry(newDN, schema, newAttributes); 265 } 266 267 268 269 /** 270 * Applies any appropriate redaction to the provided DN. 271 * 272 * @param dn The DN for which to apply any appropriate redaction. 273 * 274 * @return The DN with any appropriate redaction applied. 275 */ 276 private String redactDN(final String dn) 277 { 278 if (dn == null) 279 { 280 return null; 281 } 282 283 try 284 { 285 boolean changeApplied = false; 286 final RDN[] originalRDNs = new DN(dn).getRDNs(); 287 final RDN[] newRDNs = new RDN[originalRDNs.length]; 288 for (int i=0; i < originalRDNs.length; i++) 289 { 290 final String[] names = originalRDNs[i].getAttributeNames(); 291 final String[] originalValues = originalRDNs[i].getAttributeValues(); 292 final String[] newValues = new String[originalValues.length]; 293 for (int j=0; j < names.length; j++) 294 { 295 if (attributes.contains(StaticUtils.toLowerCase(names[j]))) 296 { 297 changeApplied = true; 298 newValues[j] = "***REDACTED***"; 299 } 300 else 301 { 302 newValues[j] = originalValues[j]; 303 } 304 } 305 newRDNs[i] = new RDN(names, newValues, schema); 306 } 307 308 if (changeApplied) 309 { 310 return new DN(newRDNs).toString(); 311 } 312 else 313 { 314 return dn; 315 } 316 } 317 catch (final Exception e) 318 { 319 Debug.debugException(e); 320 return dn; 321 } 322 } 323 324 325 326 /** 327 * {@inheritDoc} 328 */ 329 public LDIFChangeRecord transformChangeRecord(final LDIFChangeRecord r) 330 { 331 if (r == null) 332 { 333 return null; 334 } 335 336 337 // If it's an add change record, then just use the same processing as for an 338 // entry. 339 if (r instanceof LDIFAddChangeRecord) 340 { 341 final LDIFAddChangeRecord addRecord = (LDIFAddChangeRecord) r; 342 return new LDIFAddChangeRecord(transformEntry(addRecord.getEntryToAdd()), 343 addRecord.getControls()); 344 } 345 346 347 // If it's a delete change record, then see if the DN contains anything 348 // that we might need to redact. 349 if (r instanceof LDIFDeleteChangeRecord) 350 { 351 if (redactDNAttributes) 352 { 353 final LDIFDeleteChangeRecord deleteRecord = (LDIFDeleteChangeRecord) r; 354 return new LDIFDeleteChangeRecord(redactDN(deleteRecord.getDN()), 355 deleteRecord.getControls()); 356 } 357 else 358 { 359 return r; 360 } 361 } 362 363 364 // If it's a modify change record, then redact all appropriate values. 365 if (r instanceof LDIFModifyChangeRecord) 366 { 367 final LDIFModifyChangeRecord modifyRecord = (LDIFModifyChangeRecord) r; 368 369 final String newDN; 370 if (redactDNAttributes) 371 { 372 newDN = redactDN(modifyRecord.getDN()); 373 } 374 else 375 { 376 newDN = modifyRecord.getDN(); 377 } 378 379 final Modification[] originalMods = modifyRecord.getModifications(); 380 final Modification[] newMods = new Modification[originalMods.length]; 381 382 for (int i=0; i < originalMods.length; i++) 383 { 384 // If the modification doesn't have any values, then just use the 385 // original modification. 386 final Modification m = originalMods[i]; 387 if (! m.hasValue()) 388 { 389 newMods[i] = m; 390 continue; 391 } 392 393 394 // See if the modification targets an attribute that we should redact. 395 // If not, then see if the attribute has a DN syntax. 396 final String attrName = StaticUtils.toLowerCase( 397 Attribute.getBaseName(m.getAttributeName())); 398 if (! attributes.contains(attrName)) 399 { 400 if (redactDNAttributes && (schema != null) && 401 (MatchingRule.selectEqualityMatchingRule(attrName, schema) 402 instanceof DistinguishedNameMatchingRule)) 403 { 404 final String[] originalValues = m.getValues(); 405 final String[] newValues = new String[originalValues.length]; 406 for (int j=0; j < originalValues.length; j++) 407 { 408 newValues[j] = redactDN(originalValues[j]); 409 } 410 newMods[i] = new Modification(m.getModificationType(), 411 m.getAttributeName(), newValues); 412 } 413 else 414 { 415 newMods[i] = m; 416 } 417 continue; 418 } 419 420 421 // Get the original values. If there's only one of them, or if we 422 // shouldn't preserve the original number of values, then just create a 423 // modification with a single value. Otherwise, create a modification 424 // with the appropriate number of values. 425 final ASN1OctetString[] originalValues = m.getRawValues(); 426 if (preserveValueCount && (originalValues.length > 1)) 427 { 428 final ASN1OctetString[] newValues = 429 new ASN1OctetString[originalValues.length]; 430 for (int j=0; j < originalValues.length; j++) 431 { 432 newValues[j] = new ASN1OctetString("***REDACTED" + (j+1) + "***"); 433 } 434 newMods[i] = new Modification(m.getModificationType(), 435 m.getAttributeName(), newValues); 436 } 437 else 438 { 439 newMods[i] = new Modification(m.getModificationType(), 440 m.getAttributeName(), "***REDACTED***"); 441 } 442 } 443 444 return new LDIFModifyChangeRecord(newDN, newMods, 445 modifyRecord.getControls()); 446 } 447 448 449 // If it's a modify DN change record, then see if the DN, new RDN, or new 450 // superior DN contain anything that we might need to redact. 451 if (r instanceof LDIFModifyDNChangeRecord) 452 { 453 if (redactDNAttributes) 454 { 455 final LDIFModifyDNChangeRecord modDNRecord = 456 (LDIFModifyDNChangeRecord) r; 457 return new LDIFModifyDNChangeRecord(redactDN(modDNRecord.getDN()), 458 redactDN(modDNRecord.getNewRDN()), modDNRecord.deleteOldRDN(), 459 redactDN(modDNRecord.getNewSuperiorDN()), 460 modDNRecord.getControls()); 461 } 462 else 463 { 464 return r; 465 } 466 } 467 468 469 // We should never get here. 470 return r; 471 } 472 473 474 475 /** 476 * {@inheritDoc} 477 */ 478 public Entry translate(final Entry original, final long firstLineNumber) 479 { 480 return transformEntry(original); 481 } 482 483 484 485 /** 486 * {@inheritDoc} 487 */ 488 public LDIFChangeRecord translate(final LDIFChangeRecord original, 489 final long firstLineNumber) 490 { 491 return transformChangeRecord(original); 492 } 493 494 495 496 /** 497 * {@inheritDoc} 498 */ 499 public Entry translateEntryToWrite(final Entry original) 500 { 501 return transformEntry(original); 502 } 503 504 505 506 /** 507 * {@inheritDoc} 508 */ 509 public LDIFChangeRecord translateChangeRecordToWrite( 510 final LDIFChangeRecord original) 511 { 512 return transformChangeRecord(original); 513 } 514}