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