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