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}