001/* 002 * Copyright 2016-2018 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2016-2018 Ping Identity Corporation 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 @Override() 202 public Entry transformEntry(final Entry e) 203 { 204 if (e == null) 205 { 206 return null; 207 } 208 209 210 // If we should process entry DNs, then see if the DN contains any of the 211 // target attributes. 212 final String newDN; 213 if (redactDNAttributes) 214 { 215 newDN = redactDN(e.getDN()); 216 } 217 else 218 { 219 newDN = e.getDN(); 220 } 221 222 223 // Create a copy of the entry with all appropriate attributes redacted. 224 final Collection<Attribute> originalAttributes = e.getAttributes(); 225 final ArrayList<Attribute> newAttributes = 226 new ArrayList<Attribute>(originalAttributes.size()); 227 for (final Attribute a : originalAttributes) 228 { 229 final String baseName = StaticUtils.toLowerCase(a.getBaseName()); 230 if (attributes.contains(baseName)) 231 { 232 if (preserveValueCount && (a.size() > 1)) 233 { 234 final ASN1OctetString[] values = new ASN1OctetString[a.size()]; 235 for (int i=0; i < values.length; i++) 236 { 237 values[i] = new ASN1OctetString("***REDACTED" + (i+1) + "***"); 238 } 239 newAttributes.add(new Attribute(a.getName(), values)); 240 } 241 else 242 { 243 newAttributes.add(new Attribute(a.getName(), "***REDACTED***")); 244 } 245 } 246 else if (redactDNAttributes && (schema != null) && 247 (MatchingRule.selectEqualityMatchingRule(baseName, schema) 248 instanceof DistinguishedNameMatchingRule)) 249 { 250 251 final String[] originalValues = a.getValues(); 252 final String[] newValues = new String[originalValues.length]; 253 for (int i=0; i < originalValues.length; i++) 254 { 255 newValues[i] = redactDN(originalValues[i]); 256 } 257 newAttributes.add(new Attribute(a.getName(), schema, newValues)); 258 } 259 else 260 { 261 newAttributes.add(a); 262 } 263 } 264 265 return new Entry(newDN, schema, newAttributes); 266 } 267 268 269 270 /** 271 * Applies any appropriate redaction to the provided DN. 272 * 273 * @param dn The DN for which to apply any appropriate redaction. 274 * 275 * @return The DN with any appropriate redaction applied. 276 */ 277 private String redactDN(final String dn) 278 { 279 if (dn == null) 280 { 281 return null; 282 } 283 284 try 285 { 286 boolean changeApplied = false; 287 final RDN[] originalRDNs = new DN(dn).getRDNs(); 288 final RDN[] newRDNs = new RDN[originalRDNs.length]; 289 for (int i=0; i < originalRDNs.length; i++) 290 { 291 final String[] names = originalRDNs[i].getAttributeNames(); 292 final String[] originalValues = originalRDNs[i].getAttributeValues(); 293 final String[] newValues = new String[originalValues.length]; 294 for (int j=0; j < names.length; j++) 295 { 296 if (attributes.contains(StaticUtils.toLowerCase(names[j]))) 297 { 298 changeApplied = true; 299 newValues[j] = "***REDACTED***"; 300 } 301 else 302 { 303 newValues[j] = originalValues[j]; 304 } 305 } 306 newRDNs[i] = new RDN(names, newValues, schema); 307 } 308 309 if (changeApplied) 310 { 311 return new DN(newRDNs).toString(); 312 } 313 else 314 { 315 return dn; 316 } 317 } 318 catch (final Exception e) 319 { 320 Debug.debugException(e); 321 return dn; 322 } 323 } 324 325 326 327 /** 328 * {@inheritDoc} 329 */ 330 @Override() 331 public LDIFChangeRecord transformChangeRecord(final LDIFChangeRecord r) 332 { 333 if (r == null) 334 { 335 return null; 336 } 337 338 339 // If it's an add change record, then just use the same processing as for an 340 // entry. 341 if (r instanceof LDIFAddChangeRecord) 342 { 343 final LDIFAddChangeRecord addRecord = (LDIFAddChangeRecord) r; 344 return new LDIFAddChangeRecord(transformEntry(addRecord.getEntryToAdd()), 345 addRecord.getControls()); 346 } 347 348 349 // If it's a delete change record, then see if the DN contains anything 350 // that we might need to redact. 351 if (r instanceof LDIFDeleteChangeRecord) 352 { 353 if (redactDNAttributes) 354 { 355 final LDIFDeleteChangeRecord deleteRecord = (LDIFDeleteChangeRecord) r; 356 return new LDIFDeleteChangeRecord(redactDN(deleteRecord.getDN()), 357 deleteRecord.getControls()); 358 } 359 else 360 { 361 return r; 362 } 363 } 364 365 366 // If it's a modify change record, then redact all appropriate values. 367 if (r instanceof LDIFModifyChangeRecord) 368 { 369 final LDIFModifyChangeRecord modifyRecord = (LDIFModifyChangeRecord) r; 370 371 final String newDN; 372 if (redactDNAttributes) 373 { 374 newDN = redactDN(modifyRecord.getDN()); 375 } 376 else 377 { 378 newDN = modifyRecord.getDN(); 379 } 380 381 final Modification[] originalMods = modifyRecord.getModifications(); 382 final Modification[] newMods = new Modification[originalMods.length]; 383 384 for (int i=0; i < originalMods.length; i++) 385 { 386 // If the modification doesn't have any values, then just use the 387 // original modification. 388 final Modification m = originalMods[i]; 389 if (! m.hasValue()) 390 { 391 newMods[i] = m; 392 continue; 393 } 394 395 396 // See if the modification targets an attribute that we should redact. 397 // If not, then see if the attribute has a DN syntax. 398 final String attrName = StaticUtils.toLowerCase( 399 Attribute.getBaseName(m.getAttributeName())); 400 if (! attributes.contains(attrName)) 401 { 402 if (redactDNAttributes && (schema != null) && 403 (MatchingRule.selectEqualityMatchingRule(attrName, schema) 404 instanceof DistinguishedNameMatchingRule)) 405 { 406 final String[] originalValues = m.getValues(); 407 final String[] newValues = new String[originalValues.length]; 408 for (int j=0; j < originalValues.length; j++) 409 { 410 newValues[j] = redactDN(originalValues[j]); 411 } 412 newMods[i] = new Modification(m.getModificationType(), 413 m.getAttributeName(), newValues); 414 } 415 else 416 { 417 newMods[i] = m; 418 } 419 continue; 420 } 421 422 423 // Get the original values. If there's only one of them, or if we 424 // shouldn't preserve the original number of values, then just create a 425 // modification with a single value. Otherwise, create a modification 426 // with the appropriate number of values. 427 final ASN1OctetString[] originalValues = m.getRawValues(); 428 if (preserveValueCount && (originalValues.length > 1)) 429 { 430 final ASN1OctetString[] newValues = 431 new ASN1OctetString[originalValues.length]; 432 for (int j=0; j < originalValues.length; j++) 433 { 434 newValues[j] = new ASN1OctetString("***REDACTED" + (j+1) + "***"); 435 } 436 newMods[i] = new Modification(m.getModificationType(), 437 m.getAttributeName(), newValues); 438 } 439 else 440 { 441 newMods[i] = new Modification(m.getModificationType(), 442 m.getAttributeName(), "***REDACTED***"); 443 } 444 } 445 446 return new LDIFModifyChangeRecord(newDN, newMods, 447 modifyRecord.getControls()); 448 } 449 450 451 // If it's a modify DN change record, then see if the DN, new RDN, or new 452 // superior DN contain anything that we might need to redact. 453 if (r instanceof LDIFModifyDNChangeRecord) 454 { 455 if (redactDNAttributes) 456 { 457 final LDIFModifyDNChangeRecord modDNRecord = 458 (LDIFModifyDNChangeRecord) r; 459 return new LDIFModifyDNChangeRecord(redactDN(modDNRecord.getDN()), 460 redactDN(modDNRecord.getNewRDN()), modDNRecord.deleteOldRDN(), 461 redactDN(modDNRecord.getNewSuperiorDN()), 462 modDNRecord.getControls()); 463 } 464 else 465 { 466 return r; 467 } 468 } 469 470 471 // We should never get here. 472 return r; 473 } 474 475 476 477 /** 478 * {@inheritDoc} 479 */ 480 @Override() 481 public Entry translate(final Entry original, final long firstLineNumber) 482 { 483 return transformEntry(original); 484 } 485 486 487 488 /** 489 * {@inheritDoc} 490 */ 491 @Override() 492 public LDIFChangeRecord translate(final LDIFChangeRecord original, 493 final long firstLineNumber) 494 { 495 return transformChangeRecord(original); 496 } 497 498 499 500 /** 501 * {@inheritDoc} 502 */ 503 @Override() 504 public Entry translateEntryToWrite(final Entry original) 505 { 506 return transformEntry(original); 507 } 508 509 510 511 /** 512 * {@inheritDoc} 513 */ 514 @Override() 515 public LDIFChangeRecord translateChangeRecordToWrite( 516 final LDIFChangeRecord original) 517 { 518 return transformChangeRecord(original); 519 } 520}