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