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