001/*
002 * Copyright 2007-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2007-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) 2007-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.controls;
037
038
039
040import java.util.ArrayList;
041import java.util.Collection;
042import java.util.List;
043
044import com.unboundid.asn1.ASN1Element;
045import com.unboundid.asn1.ASN1OctetString;
046import com.unboundid.ldap.sdk.Attribute;
047import com.unboundid.ldap.sdk.Control;
048import com.unboundid.ldap.sdk.Entry;
049import com.unboundid.ldap.sdk.Filter;
050import com.unboundid.ldap.sdk.JSONControlDecodeHelper;
051import com.unboundid.ldap.sdk.LDAPException;
052import com.unboundid.ldap.sdk.ResultCode;
053import com.unboundid.util.Debug;
054import com.unboundid.util.NotMutable;
055import com.unboundid.util.NotNull;
056import com.unboundid.util.Nullable;
057import com.unboundid.util.ThreadSafety;
058import com.unboundid.util.ThreadSafetyLevel;
059import com.unboundid.util.Validator;
060import com.unboundid.util.json.JSONField;
061import com.unboundid.util.json.JSONObject;
062
063import static com.unboundid.ldap.sdk.controls.ControlMessages.*;
064
065
066
067/**
068 * This class provides an implementation of the LDAP assertion request control
069 * as defined in <A HREF="http://www.ietf.org/rfc/rfc4528.txt">RFC 4528</A>.  It
070 * may be used in conjunction with an add, compare, delete, modify, modify DN,
071 * or search operation.  The assertion control includes a search filter, and the
072 * associated operation should only be allowed to continue if the target entry
073 * matches the provided filter.  If the filter does not match the target entry,
074 * then the operation should fail with an
075 * {@link ResultCode#ASSERTION_FAILED} result.
076 * <BR><BR>
077 * The behavior of the assertion request control makes it ideal for atomic
078 * "check and set" types of operations, particularly when modifying an entry.
079 * For example, it can be used to ensure that when changing the value of an
080 * attribute, the current value has not been modified since it was last
081 * retrieved.
082 * <BR><BR>
083 * <H2>Example</H2>
084 * The following example demonstrates the use of the assertion request control.
085 * It shows an attempt to modify an entry's "accountBalance" attribute to set
086 * the value to "543.21" only if the current value is "1234.56":
087 * <PRE>
088 * Modification mod = new Modification(ModificationType.REPLACE,
089 *      "accountBalance", "543.21");
090 * ModifyRequest modifyRequest =
091 *      new ModifyRequest("uid=john.doe,ou=People,dc=example,dc=com", mod);
092 * modifyRequest.addControl(
093 *      new AssertionRequestControl("(accountBalance=1234.56)"));
094 *
095 * LDAPResult modifyResult;
096 * try
097 * {
098 *   modifyResult = connection.modify(modifyRequest);
099 *   // If we've gotten here, then the modification was successful.
100 * }
101 * catch (LDAPException le)
102 * {
103 *   modifyResult = le.toLDAPResult();
104 *   ResultCode resultCode = le.getResultCode();
105 *   String errorMessageFromServer = le.getDiagnosticMessage();
106 *   if (resultCode == ResultCode.ASSERTION_FAILED)
107 *   {
108 *     // The modification failed because the account balance value wasn't
109 *     // what we thought it was.
110 *   }
111 *   else
112 *   {
113 *     // The modification failed for some other reason.
114 *   }
115 * }
116 * </PRE>
117 */
118@NotMutable()
119@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
120public final class AssertionRequestControl
121       extends Control
122{
123  /**
124   * The OID (1.3.6.1.1.12) for the assertion request control.
125   */
126  @NotNull public static final String ASSERTION_REQUEST_OID = "1.3.6.1.1.12";
127
128
129
130  /**
131   * The name of the field used to represent the assertion filter in the
132   * JSON representation of this control.
133   */
134  @NotNull private static final String JSON_FIELD_FILTER = "filter";
135
136
137
138  /**
139   * The serial version UID for this serializable class.
140   */
141  private static final long serialVersionUID = 6592634203410511095L;
142
143
144
145  // The search filter for this assertion request control.
146  @NotNull private final Filter filter;
147
148
149
150  /**
151   * Creates a new assertion request control with the provided filter.  It will
152   * be marked as critical.
153   *
154   * @param  filter  The string representation of the filter for this assertion
155   *                 control.  It must not be {@code null}.
156   *
157   * @throws  LDAPException  If the provided filter string cannot be decoded as
158   *                         a search filter.
159   */
160  public AssertionRequestControl(@NotNull final String filter)
161         throws LDAPException
162  {
163    this(Filter.create(filter), true);
164  }
165
166
167
168  /**
169   * Creates a new assertion request control with the provided filter.  It will
170   * be marked as critical.
171   *
172   * @param  filter  The filter for this assertion control.  It must not be
173   *                 {@code null}.
174   */
175  public AssertionRequestControl(@NotNull final Filter filter)
176  {
177    this(filter, true);
178  }
179
180
181
182  /**
183   * Creates a new assertion request control with the provided filter.  It will
184   * be marked as critical.
185   *
186   * @param  filter      The string representation of the filter for this
187   *                     assertion control.  It must not be {@code null}.
188   * @param  isCritical  Indicates whether this control should be marked
189   *                     critical.
190   *
191   * @throws  LDAPException  If the provided filter string cannot be decoded as
192   *                         a search filter.
193   */
194  public AssertionRequestControl(@NotNull final String filter,
195                                 final boolean isCritical)
196         throws LDAPException
197  {
198    this(Filter.create(filter), isCritical);
199  }
200
201
202
203  /**
204   * Creates a new assertion request control with the provided filter.  It will
205   * be marked as critical.
206   *
207   * @param  filter      The filter for this assertion control.  It must not be
208   *                     {@code null}.
209   * @param  isCritical  Indicates whether this control should be marked
210   *                     critical.
211   */
212  public AssertionRequestControl(@NotNull final Filter filter,
213                                 final boolean isCritical)
214  {
215    super(ASSERTION_REQUEST_OID, isCritical, encodeValue(filter));
216
217    this.filter = filter;
218  }
219
220
221
222  /**
223   * Creates a new assertion request control which is decoded from the provided
224   * generic control.
225   *
226   * @param  control  The generic control to be decoded as an assertion request
227   *                  control.
228   *
229   * @throws  LDAPException  If the provided control cannot be decoded as an
230   *                         assertion request control.
231   */
232  public AssertionRequestControl(@NotNull final Control control)
233         throws LDAPException
234  {
235    super(control);
236
237    final ASN1OctetString value = control.getValue();
238    if (value == null)
239    {
240      throw new LDAPException(ResultCode.DECODING_ERROR,
241                              ERR_ASSERT_NO_VALUE.get());
242    }
243
244
245    try
246    {
247      final ASN1Element valueElement = ASN1Element.decode(value.getValue());
248      filter = Filter.decode(valueElement);
249    }
250    catch (final Exception e)
251    {
252      Debug.debugException(e);
253      throw new LDAPException(ResultCode.DECODING_ERROR,
254                              ERR_ASSERT_CANNOT_DECODE.get(e), e);
255    }
256  }
257
258
259
260  /**
261   * Generates an assertion request control that may be used to help ensure
262   * that some or all of the attributes in the specified entry have not changed
263   * since it was read from the server.
264   *
265   * @param  sourceEntry  The entry from which to take the attributes to include
266   *                      in the assertion request control.  It must not be
267   *                      {@code null} and should have at least one attribute to
268   *                      be included in the generated filter.
269   * @param  attributes   The names of the attributes to include in the
270   *                      assertion request control.  If this is empty or
271   *                      {@code null}, then all attributes in the provided
272   *                      entry will be used.
273   *
274   * @return  The generated assertion request control.
275   */
276  @NotNull()
277  public static AssertionRequestControl generate(
278                     @NotNull final Entry sourceEntry,
279                     @Nullable final String... attributes)
280  {
281    Validator.ensureNotNull(sourceEntry);
282
283    final ArrayList<Filter> andComponents;
284
285    if ((attributes == null) || (attributes.length == 0))
286    {
287      final Collection<Attribute> entryAttrs = sourceEntry.getAttributes();
288      andComponents = new ArrayList<>(entryAttrs.size());
289      for (final Attribute a : entryAttrs)
290      {
291        for (final ASN1OctetString v : a.getRawValues())
292        {
293          andComponents.add(Filter.createEqualityFilter(a.getName(),
294               v.getValue()));
295        }
296      }
297    }
298    else
299    {
300      andComponents = new ArrayList<>(attributes.length);
301      for (final String name : attributes)
302      {
303        final Attribute a = sourceEntry.getAttribute(name);
304        if (a != null)
305        {
306          for (final ASN1OctetString v : a.getRawValues())
307          {
308            andComponents.add(Filter.createEqualityFilter(name, v.getValue()));
309          }
310        }
311      }
312    }
313
314    if (andComponents.size() == 1)
315    {
316      return new AssertionRequestControl(andComponents.get(0));
317    }
318    else
319    {
320      return new AssertionRequestControl(Filter.createANDFilter(andComponents));
321    }
322  }
323
324
325
326  /**
327   * Encodes the provided information into an octet string that can be used as
328   * the value for this control.
329   *
330   * @param  filter  The filter for this assertion control.  It must not be
331   *                 {@code null}.
332   *
333   * @return  An ASN.1 octet string that can be used as the value for this
334   *          control.
335   */
336  @NotNull()
337  private static ASN1OctetString encodeValue(@NotNull final Filter filter)
338  {
339    return new ASN1OctetString(filter.encode().encode());
340  }
341
342
343
344  /**
345   * Retrieves the filter for this assertion control.
346   *
347   * @return  The filter for this assertion control.
348   */
349  @NotNull()
350  public Filter getFilter()
351  {
352    return filter;
353  }
354
355
356
357  /**
358   * {@inheritDoc}
359   */
360  @Override()
361  @NotNull()
362  public String getControlName()
363  {
364    return INFO_CONTROL_NAME_ASSERTION_REQUEST.get();
365  }
366
367
368
369  /**
370   * Retrieves a representation of this assertion request control as a JSON
371   * object.  The JSON object uses the following fields:
372   * <UL>
373   *   <LI>
374   *     {@code oid} -- A mandatory string field whose value is the object
375   *     identifier for this control.  For the assertion request control, the
376   *     OID is "1.3.6.1.1.12".
377   *   </LI>
378   *   <LI>
379   *     {@code control-name} -- An optional string field whose value is a
380   *     human-readable name for this control.  This field is only intended for
381   *     descriptive purposes, and when decoding a control, the {@code oid}
382   *     field should be used to identify the type of control.
383   *   </LI>
384   *   <LI>
385   *     {@code criticality} -- A mandatory Boolean field used to indicate
386   *     whether this control is considered critical.
387   *   </LI>
388   *   <LI>
389   *     {@code value-base64} -- An optional string field whose value is a
390   *     base64-encoded representation of the raw value for this assertion
391   *     request control.  Exactly one of the {@code value-base64} and
392   *     {@code value-json} fields must be present.
393   *   </LI>
394   *   <LI>
395   *     {@code value-json} -- An optional JSON object field whose value is a
396   *     user-friendly representation of the value for this assertion request
397   *     control.  Exactly one of the {@code value-base64} and
398   *     {@code value-json} fields must be present, and if the
399   *     {@code value-json} field is used, then it will use the following
400   *     fields:
401   *     <UL>
402   *       <LI>
403   *         {@code filter} -- A mandatory string field whose value is a string
404   *         representation of the assertion filter.
405   *       </LI>
406   *     </UL>
407   *   </LI>
408   * </UL>
409   *
410   * @return  A JSON object that contains a representation of this control.
411   */
412  @Override()
413  @NotNull()
414  public JSONObject toJSONControl()
415  {
416    return new JSONObject(
417         new JSONField(JSONControlDecodeHelper.JSON_FIELD_OID,
418              ASSERTION_REQUEST_OID),
419         new JSONField(JSONControlDecodeHelper.JSON_FIELD_CONTROL_NAME,
420              INFO_CONTROL_NAME_ASSERTION_REQUEST.get()),
421         new JSONField(JSONControlDecodeHelper.JSON_FIELD_CRITICALITY,
422              isCritical()),
423         new JSONField(JSONControlDecodeHelper.JSON_FIELD_VALUE_JSON,
424              new JSONObject(
425                   new JSONField(JSON_FIELD_FILTER, filter.toString()))));
426  }
427
428
429
430  /**
431   * Attempts to decode the provided object as a JSON representation of an
432   * assertion request control.
433   *
434   * @param  controlObject  The JSON object to be decoded.  It must not be
435   *                        {@code null}.
436   * @param  strict         Indicates whether to use strict mode when decoding
437   *                        the provided JSON object.  If this is {@code true},
438   *                        then this method will throw an exception if the
439   *                        provided JSON object contains any unrecognized
440   *                        fields.  If this is {@code false}, then unrecognized
441   *                        fields will be ignored.
442   *
443   * @return  The assertion request control that was decoded from the provided
444   *          JSON object.
445   *
446   * @throws  LDAPException  If the provided JSON object cannot be parsed as a
447   *                         valid assertion request control.
448   */
449  @NotNull()
450  public static AssertionRequestControl decodeJSONControl(
451              @NotNull final JSONObject controlObject,
452              final boolean strict)
453         throws LDAPException
454  {
455    final JSONControlDecodeHelper jsonControl = new JSONControlDecodeHelper(
456         controlObject, strict, true, true);
457
458    final ASN1OctetString rawValue = jsonControl.getRawValue();
459    if (rawValue != null)
460    {
461      return new AssertionRequestControl(new Control(
462           jsonControl.getOID(), jsonControl.getCriticality(),
463           rawValue));
464    }
465
466    final JSONObject valueObject = jsonControl.getValueObject();
467    final String filterString = valueObject.getFieldAsString(JSON_FIELD_FILTER);
468    if (filterString == null)
469    {
470      throw new LDAPException(ResultCode.DECODING_ERROR,
471           ERR_ASSERT_JSON_CONTROL_MISSING_FILTER.get(
472                controlObject.toSingleLineString(), JSON_FIELD_FILTER));
473    }
474
475    final Filter parsedFilter;
476    try
477    {
478      parsedFilter = Filter.create(filterString);
479    }
480    catch (final LDAPException e)
481    {
482      Debug.debugException(e);
483      throw new LDAPException(ResultCode.DECODING_ERROR,
484           ERR_ASSERT_JSON_CONTROL_INVALID_FILTER.get(
485                controlObject.toSingleLineString(), filterString),
486           e);
487    }
488
489    if (strict)
490    {
491      final List<String> unrecognizedFields =
492           JSONControlDecodeHelper.getControlObjectUnexpectedFields(
493                valueObject, JSON_FIELD_FILTER);
494      if (! unrecognizedFields.isEmpty())
495      {
496        throw new LDAPException(ResultCode.DECODING_ERROR,
497             ERR_ASSERT_JSON_CONTROL_UNRECOGNIZED_FIELD.get(
498                  controlObject.toSingleLineString(),
499                  unrecognizedFields.get(0)));
500      }
501    }
502
503    return new AssertionRequestControl(parsedFilter,
504         jsonControl.getCriticality());
505  }
506
507
508
509  /**
510   * {@inheritDoc}
511   */
512  @Override()
513  public void toString(@NotNull final StringBuilder buffer)
514  {
515    buffer.append("AssertionRequestControl(filter='");
516    filter.toString(buffer);
517    buffer.append("', isCritical=");
518    buffer.append(isCritical());
519    buffer.append(')');
520  }
521}