001/*
002 * Copyright 2012-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2012-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) 2012-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.unboundidds.controls;
037
038
039
040import java.util.List;
041import java.util.ArrayList;
042
043import com.unboundid.asn1.ASN1Element;
044import com.unboundid.asn1.ASN1Sequence;
045import com.unboundid.ldap.matchingrules.BooleanMatchingRule;
046import com.unboundid.ldap.matchingrules.OctetStringMatchingRule;
047import com.unboundid.ldap.sdk.Control;
048import com.unboundid.ldap.sdk.AddRequest;
049import com.unboundid.ldap.sdk.Attribute;
050import com.unboundid.ldap.sdk.JSONControlDecodeHelper;
051import com.unboundid.ldap.sdk.LDAPException;
052import com.unboundid.ldap.sdk.Modification;
053import com.unboundid.ldap.sdk.ResultCode;
054import com.unboundid.ldif.LDIFModifyChangeRecord;
055import com.unboundid.util.Debug;
056import com.unboundid.util.NotMutable;
057import com.unboundid.util.NotNull;
058import com.unboundid.util.Nullable;
059import com.unboundid.util.StaticUtils;
060import com.unboundid.util.ThreadSafety;
061import com.unboundid.util.ThreadSafetyLevel;
062import com.unboundid.util.json.JSONField;
063import com.unboundid.util.json.JSONObject;
064
065import static com.unboundid.ldap.sdk.unboundidds.controls.ControlMessages.*;
066
067
068
069/**
070 * This class provides a request control which may be included in an add request
071 * to indicate that the contents of the resulting entry should come not from the
072 * data of the add request itself but instead from a soft-deleted entry.  This
073 * can be used to recover an entry that was previously removed by a delete
074 * request containing the {@link SoftDeleteRequestControl}.
075 * <BR>
076 * <BLOCKQUOTE>
077 *   <B>NOTE:</B>  This class, and other classes within the
078 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
079 *   supported for use against Ping Identity, UnboundID, and
080 *   Nokia/Alcatel-Lucent 8661 server products.  These classes provide support
081 *   for proprietary functionality or for external specifications that are not
082 *   considered stable or mature enough to be guaranteed to work in an
083 *   interoperable way with other types of LDAP servers.
084 * </BLOCKQUOTE>
085 * <BR>
086 * The criticality for this control should always be {@code TRUE}.  The
087 * criticality will have no effect on servers that do support this control, but
088 * a criticality of {@code TRUE} will ensure that a server which does not
089 * support soft deletes does not attempt to process the add request.  If the
090 * criticality were {@code FALSE}, then any server that does not support the
091 * control would simply ignore it and attempt to add the entry specified in the
092 * add request (which will have details about the undelete to be processed).
093 * <BR><BR>
094 * The control may optionally have a value.  If a value is provided, then it
095 * must be the encoded representation of an empty ASN.1 sequence, like:
096 * <PRE>
097 *   UndeleteRequestValue ::= SEQUENCE {
098 *     ... }
099 * </PRE>
100 * In the future, the value sequence may allow one or more elements to customize
101 * the behavior of the undelete operation, but at present no such elements are
102 * defined.
103 * See the documentation for the {@link SoftDeleteRequestControl} class for an
104 * example demonstrating the use of this control.
105 *
106 * @see  HardDeleteRequestControl
107 * @see  SoftDeleteRequestControl
108 */
109@NotMutable()
110@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
111public final class UndeleteRequestControl
112       extends Control
113{
114  /**
115   * The OID (1.3.6.1.4.1.30221.2.5.23) for the undelete request control.
116   */
117  @NotNull public static final String UNDELETE_REQUEST_OID =
118       "1.3.6.1.4.1.30221.2.5.23";
119
120
121
122  /**
123   * The name of the optional attribute used to specify a set of changes to
124   * apply to the soft-deleted entry during the course of the undelete.
125   */
126  @NotNull public static final String ATTR_CHANGES = "ds-undelete-changes";
127
128
129
130  /**
131   * The name of the optional attribute used to indicate whether the
132   * newly-undeleted user account should be disabled and prevented from
133   * authenticating.
134   */
135  @NotNull public static final String ATTR_DISABLE_ACCOUNT =
136       "ds-undelete-disable-account";
137
138
139
140  /**
141   * The name of the optional attribute used to indicate whether the
142   * newly-undeleted user will be required to change his/her password
143   * immediately after authenticating and before being required to request any
144   * other operations.
145   */
146  @NotNull public static final String ATTR_MUST_CHANGE_PASSWORD =
147       "ds-undelete-must-change-password";
148
149
150
151  /**
152   * The name of the optional attribute used to specify the new password for use
153   * in the newly-undeleted entry.
154   */
155  @NotNull public static final String ATTR_NEW_PASSWORD =
156       "ds-undelete-new-password";
157
158
159
160  /**
161   * The name of the optional attribute used to specify the password currently
162   * contained in the soft-deleted entry, to be validated as part of the
163   * undelete process.
164   */
165  @NotNull public static final String ATTR_OLD_PASSWORD =
166       "ds-undelete-old-password";
167
168
169
170  /**
171   * The name of the required attribute used to specify the DN of the
172   * soft-deleted entry to be undeleted.
173   */
174  @NotNull public static final String ATTR_SOFT_DELETED_ENTRY_DN =
175       "ds-undelete-from-dn";
176
177
178
179  /**
180   * The serial version UID for this serializable class.
181   */
182  private static final long serialVersionUID = 5338045977962112876L;
183
184
185
186  /**
187   * Creates a undelete request control with a criticality of TRUE and no value.
188   */
189  public UndeleteRequestControl()
190  {
191    super(UNDELETE_REQUEST_OID, true, null);
192  }
193
194
195
196  /**
197   * Creates a new undelete request control which is decoded from the
198   * provided generic control.
199   *
200   * @param  control  The generic control to be decoded as an undelete request
201   *                  control.
202   *
203   * @throws  LDAPException  If the provided control cannot be decoded as an
204   *                         undelete request control.
205   */
206  public UndeleteRequestControl(@NotNull final Control control)
207         throws LDAPException
208  {
209    super(control);
210
211    if (control.hasValue())
212    {
213      try
214      {
215        final ASN1Sequence valueSequence =
216             ASN1Sequence.decodeAsSequence(control.getValue().getValue());
217        final ASN1Element[] elements = valueSequence.elements();
218        if (elements.length > 0)
219        {
220          throw new LDAPException(ResultCode.DECODING_ERROR,
221               ERR_UNDELETE_REQUEST_UNSUPPORTED_VALUE_ELEMENT_TYPE.get(
222                    StaticUtils.toHex(elements[0].getType())));
223        }
224      }
225      catch (final LDAPException le)
226      {
227        Debug.debugException(le);
228        throw le;
229      }
230      catch (final Exception e)
231      {
232        Debug.debugException(e);
233        throw new LDAPException(ResultCode.DECODING_ERROR,
234             ERR_UNDELETE_REQUEST_CANNOT_DECODE_VALUE.get(
235                  StaticUtils.getExceptionMessage(e)),
236             e);
237      }
238    }
239  }
240
241
242
243  /**
244   * Creates a new undelete request that may be used to recover the specified
245   * soft-deleted entry.
246   *
247   * @param  targetDN            The DN to use for the entry recovered
248   *                             from the soft-deleted entry contents.  It must
249   *                             not be {@code null}.
250   * @param  softDeletedEntryDN  The DN of the soft-deleted entry to be used in
251   *                             the restore process.  It must not be
252   *                             {@code null}.
253   *
254   * @return  An add request with an appropriate set of content
255   */
256  @NotNull()
257  public static AddRequest createUndeleteRequest(@NotNull final String targetDN,
258                                @NotNull final String softDeletedEntryDN)
259  {
260    return createUndeleteRequest(targetDN, softDeletedEntryDN, null, null, null,
261         null, null);
262  }
263
264
265
266  /**
267   * Creates a new undelete request that may be used to recover the specified
268   * soft-deleted entry.
269   *
270   * @param  targetDN            The DN to use for the entry recovered
271   *                             from the soft-deleted entry contents.  It must
272   *                             not be {@code null}.
273   * @param  softDeletedEntryDN  The DN of the soft-deleted entry to be used in
274   *                             the restore process.  It must not be
275   *                             {@code null}.
276   * @param  changes             An optional set of changes that should be
277   *                             applied to the entry during the course of
278   *                             undelete processing.  It may be {@code null} or
279   *                             empty if this element should be omitted from
280   *                             the resulting add request.
281   * @param  oldPassword         An optional copy of the password currently
282   *                             contained in the soft-deleted entry to be
283   *                             recovered.  If this is non-{@code null}, then
284   *                             this password will be required to match that
285   *                             contained in the target entry for the undelete
286   *                             to succeed.
287   * @param  newPassword         An optional new password to set for the user
288   *                             as part of the undelete processing.  It may be
289   *                             {@code null} if no new password should be
290   *                             provided.
291   * @param  mustChangePassword  Indicates whether the recovered user will be
292   *                             required to change his/her password before
293   *                             being allowed to request any other operations.
294   *                             It may be {@code null} if this should be
295   *                             omitted from the resulting add request.
296   * @param  disableAccount      Indicates whether the undeleted entry should be
297   *                             made disabled so that it cannot be used to
298   *                             authenticate.  It may be {@code null} if this
299   *                             should be omitted from the resulting add
300   *                             request.
301   *
302   * @return  An add request with an appropriate set of content
303   */
304  @NotNull()
305  public static AddRequest createUndeleteRequest(@NotNull final String targetDN,
306                                @NotNull final String softDeletedEntryDN,
307                                @Nullable final List<Modification> changes,
308                                @Nullable final String oldPassword,
309                                @Nullable final String newPassword,
310                                @Nullable final Boolean mustChangePassword,
311                                @Nullable final Boolean disableAccount)
312  {
313    final ArrayList<Attribute> attributes = new ArrayList<>(6);
314    attributes.add(new Attribute(ATTR_SOFT_DELETED_ENTRY_DN,
315         softDeletedEntryDN));
316
317    if ((changes != null) && (! changes.isEmpty()))
318    {
319      // The changes attribute should be an LDIF-encoded representation of the
320      // modification, with the first two lines (the DN and changetype)
321      // removed.
322      final LDIFModifyChangeRecord changeRecord =
323           new LDIFModifyChangeRecord(targetDN, changes);
324      final String[] modLdifLines = changeRecord.toLDIF(0);
325      final StringBuilder modLDIFBuffer = new StringBuilder();
326      for (int i=2; i < modLdifLines.length; i++)
327      {
328        modLDIFBuffer.append(modLdifLines[i]);
329        modLDIFBuffer.append(StaticUtils.EOL);
330      }
331      attributes.add(new Attribute(ATTR_CHANGES,
332           OctetStringMatchingRule.getInstance(), modLDIFBuffer.toString()));
333    }
334
335    if (oldPassword != null)
336    {
337      attributes.add(new Attribute(ATTR_OLD_PASSWORD,
338           OctetStringMatchingRule.getInstance(), oldPassword));
339    }
340
341    if (newPassword != null)
342    {
343      attributes.add(new Attribute(ATTR_NEW_PASSWORD,
344           OctetStringMatchingRule.getInstance(), newPassword));
345    }
346
347    if (mustChangePassword != null)
348    {
349      attributes.add(new Attribute(ATTR_MUST_CHANGE_PASSWORD,
350           BooleanMatchingRule.getInstance(),
351           (mustChangePassword ? "true" : "false")));
352    }
353
354    if (disableAccount != null)
355    {
356      attributes.add(new Attribute(ATTR_DISABLE_ACCOUNT,
357           BooleanMatchingRule.getInstance(),
358           (disableAccount ? "true" : "false")));
359    }
360
361    final Control[] controls =
362    {
363      new UndeleteRequestControl()
364    };
365
366    return new AddRequest(targetDN, attributes, controls);
367  }
368
369
370
371  /**
372   * {@inheritDoc}
373   */
374  @Override()
375  @NotNull()
376  public String getControlName()
377  {
378    return INFO_CONTROL_NAME_UNDELETE_REQUEST.get();
379  }
380
381
382
383  /**
384   * Retrieves a representation of this undelete request control as a JSON
385   * object.  The JSON object uses the following fields (note that since this
386   * control does not have a value, neither the {@code value-base64} nor
387   * {@code value-json} fields may be present):
388   * <UL>
389   *   <LI>
390   *     {@code oid} -- A mandatory string field whose value is the object
391   *     identifier for this control.  For the undelete request control, the OID
392   *     is "1.3.6.1.4.1.30221.2.5.23".
393   *   </LI>
394   *   <LI>
395   *     {@code control-name} -- An optional string field whose value is a
396   *     human-readable name for this control.  This field is only intended for
397   *     descriptive purposes, and when decoding a control, the {@code oid}
398   *     field should be used to identify the type of control.
399   *   </LI>
400   *   <LI>
401   *     {@code criticality} -- A mandatory Boolean field used to indicate
402   *     whether this control is considered critical.
403   *   </LI>
404   * </UL>
405   *
406   * @return  A JSON object that contains a representation of this control.
407   */
408  @Override()
409  @NotNull()
410  public JSONObject toJSONControl()
411  {
412    return new JSONObject(
413         new JSONField(JSONControlDecodeHelper.JSON_FIELD_OID,
414              UNDELETE_REQUEST_OID),
415         new JSONField(JSONControlDecodeHelper.JSON_FIELD_CONTROL_NAME,
416              INFO_CONTROL_NAME_UNDELETE_REQUEST.get()),
417         new JSONField(JSONControlDecodeHelper.JSON_FIELD_CRITICALITY,
418              isCritical()));
419  }
420
421
422
423  /**
424   * Attempts to decode the provided object as a JSON representation of an
425   * undelete request control.
426   *
427   * @param  controlObject  The JSON object to be decoded.  It must not be
428   *                        {@code null}.
429   * @param  strict         Indicates whether to use strict mode when decoding
430   *                        the provided JSON object.  If this is {@code true},
431   *                        then this method will throw an exception if the
432   *                        provided JSON object contains any unrecognized
433   *                        fields.  If this is {@code false}, then unrecognized
434   *                        fields will be ignored.
435   *
436   * @return  The undelete request control that was decoded from the provided
437   *          JSON object.
438   *
439   * @throws  LDAPException  If the provided JSON object cannot be parsed as a
440   *                         valid undelete request control.
441   */
442  @NotNull()
443  public static UndeleteRequestControl decodeJSONControl(
444              @NotNull final JSONObject controlObject,
445              final boolean strict)
446         throws LDAPException
447  {
448    final JSONControlDecodeHelper jsonControl = new JSONControlDecodeHelper(
449         controlObject, strict, false, false);
450
451    return new UndeleteRequestControl();
452  }
453
454
455
456  /**
457   * {@inheritDoc}
458   */
459  @Override()
460  public void toString(@NotNull final StringBuilder buffer)
461  {
462    buffer.append("UndeleteRequestControl(isCritical=");
463    buffer.append(isCritical());
464    buffer.append(')');
465  }
466}