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