001/*
002 * Copyright 2016-2018 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2016-2018 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 = new HashSet<>(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  @Override()
202  public Entry transformEntry(final Entry e)
203  {
204    if (e == null)
205    {
206      return null;
207    }
208
209
210    // If we should process entry DNs, then see if the DN contains any of the
211    // target attributes.
212    final String newDN;
213    if (redactDNAttributes)
214    {
215      newDN = redactDN(e.getDN());
216    }
217    else
218    {
219      newDN = e.getDN();
220    }
221
222
223    // Create a copy of the entry with all appropriate attributes redacted.
224    final Collection<Attribute> originalAttributes = e.getAttributes();
225    final ArrayList<Attribute> newAttributes =
226         new ArrayList<>(originalAttributes.size());
227    for (final Attribute a : originalAttributes)
228    {
229      final String baseName = StaticUtils.toLowerCase(a.getBaseName());
230      if (attributes.contains(baseName))
231      {
232        if (preserveValueCount && (a.size() > 1))
233        {
234          final ASN1OctetString[] values = new ASN1OctetString[a.size()];
235          for (int i=0; i < values.length; i++)
236          {
237            values[i] = new ASN1OctetString("***REDACTED" + (i+1) + "***");
238          }
239          newAttributes.add(new Attribute(a.getName(), values));
240        }
241        else
242        {
243          newAttributes.add(new Attribute(a.getName(), "***REDACTED***"));
244        }
245      }
246      else if (redactDNAttributes && (schema != null) &&
247           (MatchingRule.selectEqualityMatchingRule(baseName, schema)
248                instanceof DistinguishedNameMatchingRule))
249      {
250
251        final String[] originalValues = a.getValues();
252        final String[] newValues = new String[originalValues.length];
253        for (int i=0; i < originalValues.length; i++)
254        {
255          newValues[i] = redactDN(originalValues[i]);
256        }
257        newAttributes.add(new Attribute(a.getName(), schema, newValues));
258      }
259      else
260      {
261        newAttributes.add(a);
262      }
263    }
264
265    return new Entry(newDN, schema, newAttributes);
266  }
267
268
269
270  /**
271   * Applies any appropriate redaction to the provided DN.
272   *
273   * @param  dn  The DN for which to apply any appropriate redaction.
274   *
275   * @return  The DN with any appropriate redaction applied.
276   */
277  private String redactDN(final String dn)
278  {
279    if (dn == null)
280    {
281      return null;
282    }
283
284    try
285    {
286      boolean changeApplied = false;
287      final RDN[] originalRDNs = new DN(dn).getRDNs();
288      final RDN[] newRDNs = new RDN[originalRDNs.length];
289      for (int i=0; i < originalRDNs.length; i++)
290      {
291        final String[] names = originalRDNs[i].getAttributeNames();
292        final String[] originalValues = originalRDNs[i].getAttributeValues();
293        final String[] newValues = new String[originalValues.length];
294        for (int j=0; j < names.length; j++)
295        {
296          if (attributes.contains(StaticUtils.toLowerCase(names[j])))
297          {
298            changeApplied = true;
299            newValues[j] = "***REDACTED***";
300          }
301          else
302          {
303            newValues[j] = originalValues[j];
304          }
305        }
306        newRDNs[i] = new RDN(names, newValues, schema);
307      }
308
309      if (changeApplied)
310      {
311        return new DN(newRDNs).toString();
312      }
313      else
314      {
315        return dn;
316      }
317    }
318    catch (final Exception e)
319    {
320      Debug.debugException(e);
321      return dn;
322    }
323  }
324
325
326
327  /**
328   * {@inheritDoc}
329   */
330  @Override()
331  public LDIFChangeRecord transformChangeRecord(final LDIFChangeRecord r)
332  {
333    if (r == null)
334    {
335      return null;
336    }
337
338
339    // If it's an add change record, then just use the same processing as for an
340    // entry.
341    if (r instanceof LDIFAddChangeRecord)
342    {
343      final LDIFAddChangeRecord addRecord = (LDIFAddChangeRecord) r;
344      return new LDIFAddChangeRecord(transformEntry(addRecord.getEntryToAdd()),
345           addRecord.getControls());
346    }
347
348
349    // If it's a delete change record, then see if the DN contains anything
350    // that we might need to redact.
351    if (r instanceof LDIFDeleteChangeRecord)
352    {
353      if (redactDNAttributes)
354      {
355        final LDIFDeleteChangeRecord deleteRecord = (LDIFDeleteChangeRecord) r;
356        return new LDIFDeleteChangeRecord(redactDN(deleteRecord.getDN()),
357             deleteRecord.getControls());
358      }
359      else
360      {
361        return r;
362      }
363    }
364
365
366    // If it's a modify change record, then redact all appropriate values.
367    if (r instanceof LDIFModifyChangeRecord)
368    {
369      final LDIFModifyChangeRecord modifyRecord = (LDIFModifyChangeRecord) r;
370
371      final String newDN;
372      if (redactDNAttributes)
373      {
374        newDN = redactDN(modifyRecord.getDN());
375      }
376      else
377      {
378        newDN = modifyRecord.getDN();
379      }
380
381      final Modification[] originalMods = modifyRecord.getModifications();
382      final Modification[] newMods = new Modification[originalMods.length];
383
384      for (int i=0; i < originalMods.length; i++)
385      {
386        // If the modification doesn't have any values, then just use the
387        // original modification.
388        final Modification m = originalMods[i];
389        if (! m.hasValue())
390        {
391          newMods[i] = m;
392          continue;
393        }
394
395
396        // See if the modification targets an attribute that we should redact.
397        // If not, then see if the attribute has a DN syntax.
398        final String attrName = StaticUtils.toLowerCase(
399             Attribute.getBaseName(m.getAttributeName()));
400        if (! attributes.contains(attrName))
401        {
402          if (redactDNAttributes && (schema != null) &&
403               (MatchingRule.selectEqualityMatchingRule(attrName, schema)
404                instanceof DistinguishedNameMatchingRule))
405          {
406            final String[] originalValues = m.getValues();
407            final String[] newValues = new String[originalValues.length];
408            for (int j=0; j < originalValues.length; j++)
409            {
410              newValues[j] = redactDN(originalValues[j]);
411            }
412            newMods[i] = new Modification(m.getModificationType(),
413                 m.getAttributeName(), newValues);
414          }
415          else
416          {
417            newMods[i] = m;
418          }
419          continue;
420        }
421
422
423        // Get the original values.  If there's only one of them, or if we
424        // shouldn't preserve the original number of values, then just create a
425        // modification with a single value.  Otherwise, create a modification
426        // with the appropriate number of values.
427        final ASN1OctetString[] originalValues = m.getRawValues();
428        if (preserveValueCount && (originalValues.length > 1))
429        {
430          final ASN1OctetString[] newValues =
431               new ASN1OctetString[originalValues.length];
432          for (int j=0; j < originalValues.length; j++)
433          {
434            newValues[j] = new ASN1OctetString("***REDACTED" + (j+1) + "***");
435          }
436          newMods[i] = new Modification(m.getModificationType(),
437               m.getAttributeName(), newValues);
438        }
439        else
440        {
441          newMods[i] = new Modification(m.getModificationType(),
442               m.getAttributeName(), "***REDACTED***");
443        }
444      }
445
446      return new LDIFModifyChangeRecord(newDN, newMods,
447           modifyRecord.getControls());
448    }
449
450
451    // If it's a modify DN change record, then see if the DN, new RDN, or new
452    // superior DN contain anything that we might need to redact.
453    if (r instanceof LDIFModifyDNChangeRecord)
454    {
455      if (redactDNAttributes)
456      {
457        final LDIFModifyDNChangeRecord modDNRecord =
458             (LDIFModifyDNChangeRecord) r;
459        return new LDIFModifyDNChangeRecord(redactDN(modDNRecord.getDN()),
460             redactDN(modDNRecord.getNewRDN()), modDNRecord.deleteOldRDN(),
461             redactDN(modDNRecord.getNewSuperiorDN()),
462             modDNRecord.getControls());
463      }
464      else
465      {
466        return r;
467      }
468    }
469
470
471    // We should never get here.
472    return r;
473  }
474
475
476
477  /**
478   * {@inheritDoc}
479   */
480  @Override()
481  public Entry translate(final Entry original, final long firstLineNumber)
482  {
483    return transformEntry(original);
484  }
485
486
487
488  /**
489   * {@inheritDoc}
490   */
491  @Override()
492  public LDIFChangeRecord translate(final LDIFChangeRecord original,
493                                    final long firstLineNumber)
494  {
495    return transformChangeRecord(original);
496  }
497
498
499
500  /**
501   * {@inheritDoc}
502   */
503  @Override()
504  public Entry translateEntryToWrite(final Entry original)
505  {
506    return transformEntry(original);
507  }
508
509
510
511  /**
512   * {@inheritDoc}
513   */
514  @Override()
515  public LDIFChangeRecord translateChangeRecordToWrite(
516                               final LDIFChangeRecord original)
517  {
518    return transformChangeRecord(original);
519  }
520}