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}