001 /*
002 * Copyright 2016 UnboundID Corp.
003 * All Rights Reserved.
004 */
005 /*
006 * Copyright (C) 2016 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 */
021 package com.unboundid.ldap.sdk.transformations;
022
023
024
025 import java.util.ArrayList;
026 import java.util.Collection;
027 import java.util.Collections;
028 import java.util.HashSet;
029 import java.util.Set;
030
031 import com.unboundid.asn1.ASN1OctetString;
032 import com.unboundid.ldap.matchingrules.DistinguishedNameMatchingRule;
033 import com.unboundid.ldap.matchingrules.MatchingRule;
034 import com.unboundid.ldap.sdk.Attribute;
035 import com.unboundid.ldap.sdk.DN;
036 import com.unboundid.ldap.sdk.Entry;
037 import com.unboundid.ldap.sdk.Modification;
038 import com.unboundid.ldap.sdk.RDN;
039 import com.unboundid.ldap.sdk.schema.AttributeTypeDefinition;
040 import com.unboundid.ldap.sdk.schema.Schema;
041 import com.unboundid.ldif.LDIFAddChangeRecord;
042 import com.unboundid.ldif.LDIFChangeRecord;
043 import com.unboundid.ldif.LDIFDeleteChangeRecord;
044 import com.unboundid.ldif.LDIFModifyChangeRecord;
045 import com.unboundid.ldif.LDIFModifyDNChangeRecord;
046 import com.unboundid.util.Debug;
047 import com.unboundid.util.StaticUtils;
048 import com.unboundid.util.ThreadSafety;
049 import 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)
061 public 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 }