001    /*
002     * Copyright 2016 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2016 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     */
021    package com.unboundid.ldap.sdk.transformations;
022    
023    
024    
025    import java.util.ArrayList;
026    import java.util.Collection;
027    import java.util.Collections;
028    import java.util.HashSet;
029    import java.util.Set;
030    
031    import com.unboundid.asn1.ASN1OctetString;
032    import com.unboundid.ldap.matchingrules.DistinguishedNameMatchingRule;
033    import com.unboundid.ldap.matchingrules.MatchingRule;
034    import com.unboundid.ldap.sdk.Attribute;
035    import com.unboundid.ldap.sdk.DN;
036    import com.unboundid.ldap.sdk.Entry;
037    import com.unboundid.ldap.sdk.Modification;
038    import com.unboundid.ldap.sdk.RDN;
039    import com.unboundid.ldap.sdk.schema.AttributeTypeDefinition;
040    import com.unboundid.ldap.sdk.schema.Schema;
041    import com.unboundid.ldif.LDIFAddChangeRecord;
042    import com.unboundid.ldif.LDIFChangeRecord;
043    import com.unboundid.ldif.LDIFDeleteChangeRecord;
044    import com.unboundid.ldif.LDIFModifyChangeRecord;
045    import com.unboundid.ldif.LDIFModifyDNChangeRecord;
046    import com.unboundid.util.Debug;
047    import com.unboundid.util.StaticUtils;
048    import com.unboundid.util.ThreadSafety;
049    import 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)
061    public 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    }