001/*
002 * Copyright 2016-2017 UnboundID Corp.
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2016-2017 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 */
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 = 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}