From b06e01526fe898f0d146f5af65434cf88cc56d4a Mon Sep 17 00:00:00 2001 From: Valera V Harseko Date: Thu, 30 Jan 2025 12:31:43 +0300 Subject: [PATCH] [#462] RFC5805 Lightweight Directory Access Protocol (LDAP) Transactions --- .../AbortedTransactionExtendedResult.java | 94 +++++++ .../EndTransactionExtendedRequest.java | 182 ++++++++++++ .../EndTransactionExtendedResult.java | 168 +++++++++++ .../StartTransactionExtendedRequest.java | 101 +++++++ .../StartTransactionExtendedResult.java | 96 +++++++ ...ransactionSpecificationRequestControl.java | 82 ++++++ .../reference/appendix-standards.adoc | 9 +- ...nExtendedOperationHandlerConfiguration.xml | 75 +++++ ...nExtendedOperationHandlerConfiguration.xml | 55 ++++ .../resource/config/config.ldif | 17 ++ .../resource/schema/02-config.ldif | 13 +- .../opends/server/api/ClientConnection.java | 52 +++- .../EndTransactionExtendedOperation.java | 131 +++++++++ .../StartTransactionExtendedOperation.java | 66 +++++ .../extensions/TraditionalWorkerThread.java | 29 +- .../server/types/AbstractOperation.java | 14 + .../types/operation/RollbackOperation.java | 26 ++ .../LocalBackendAddOperation.java | 25 +- .../LocalBackendDeleteOperation.java | 24 +- .../LocalBackendModifyDNOperation.java | 24 +- .../LocalBackendModifyOperation.java | 24 +- .../opendj/Rfc5808TestCase.java | 260 ++++++++++++++++++ 22 files changed, 1532 insertions(+), 35 deletions(-) create mode 100644 opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/AbortedTransactionExtendedResult.java create mode 100644 opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/EndTransactionExtendedRequest.java create mode 100644 opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/EndTransactionExtendedResult.java create mode 100644 opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/StartTransactionExtendedRequest.java create mode 100644 opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/StartTransactionExtendedResult.java create mode 100644 opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/TransactionSpecificationRequestControl.java create mode 100644 opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/EndTransactionExtendedOperationHandlerConfiguration.xml create mode 100644 opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/StartTransactionExtendedOperationHandlerConfiguration.xml create mode 100644 opendj-server-legacy/src/main/java/org/opends/server/extensions/EndTransactionExtendedOperation.java create mode 100644 opendj-server-legacy/src/main/java/org/opends/server/extensions/StartTransactionExtendedOperation.java create mode 100644 opendj-server-legacy/src/main/java/org/opends/server/types/operation/RollbackOperation.java create mode 100644 opendj-server-legacy/src/test/java/org/openidentityplatform/opendj/Rfc5808TestCase.java diff --git a/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/AbortedTransactionExtendedResult.java b/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/AbortedTransactionExtendedResult.java new file mode 100644 index 0000000000..35c4981de5 --- /dev/null +++ b/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/AbortedTransactionExtendedResult.java @@ -0,0 +1,94 @@ +/* + * The contents of this file are subject to the terms of the Common Development and + * Distribution License (the License). You may not use this file except in compliance with the + * License. + * + * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the + * specific language governing permission and limitations under the License. + * + * When distributing Covered Software, include this CDDL Header Notice in each file and include + * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL + * Header, with the fields enclosed by brackets [] replaced by your own identifying + * information: "Portions Copyright [year] [name of copyright owner]". + * + * Copyright 2025 3A Systems, LLC + */ +package com.forgerock.opendj.ldap.extensions; + +import org.forgerock.opendj.io.ASN1; +import org.forgerock.opendj.io.ASN1Writer; +import org.forgerock.opendj.ldap.ByteString; +import org.forgerock.opendj.ldap.ByteStringBuilder; +import org.forgerock.opendj.ldap.ResultCode; +import org.forgerock.opendj.ldap.responses.AbstractExtendedResult; +import org.forgerock.util.Reject; + +import java.io.IOException; + +/* + The Aborted Transaction Notice is an Unsolicited Notification message + where the responseName is 1.3.6.1.1.21.4 and responseValue is present + and contains a transaction identifier. + */ +public class AbortedTransactionExtendedResult extends AbstractExtendedResult { + @Override + public String getOID() { + return "1.3.6.1.1.21.4"; + } + + private AbortedTransactionExtendedResult(final ResultCode resultCode) { + super(resultCode); + } + + public static AbortedTransactionExtendedResult newResult(final ResultCode resultCode) { + Reject.ifNull(resultCode); + return new AbortedTransactionExtendedResult(resultCode); + } + + private String transactionID = null; + + public AbortedTransactionExtendedResult setTransactionID(final String transactionID) { + this.transactionID = transactionID; + return this; + } + + public String getTransactionID() { + return transactionID; + } + + @Override + public ByteString getValue() { + final ByteStringBuilder buffer = new ByteStringBuilder(); + final ASN1Writer writer = ASN1.getWriter(buffer); + try { + writer.writeOctetString(transactionID); + } catch (final IOException ioe) { + throw new RuntimeException(ioe); + } + return buffer.toByteString(); + } + + @Override + public boolean hasValue() { + return true; + } + + @Override + public String toString() { + return "AbortedTransactionExtendedResult(resultCode=" + + getResultCode() + + ", matchedDN=" + + getMatchedDN() + + ", diagnosticMessage=" + + getDiagnosticMessage() + + ", referrals=" + + getReferralURIs() + + ", responseName=" + + getOID() + + ", transactionID=" + + transactionID + + ", controls=" + + getControls() + + ")"; + } +} diff --git a/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/EndTransactionExtendedRequest.java b/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/EndTransactionExtendedRequest.java new file mode 100644 index 0000000000..5b898a1a22 --- /dev/null +++ b/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/EndTransactionExtendedRequest.java @@ -0,0 +1,182 @@ +/* + * The contents of this file are subject to the terms of the Common Development and + * Distribution License (the License). You may not use this file except in compliance with the + * License. + * + * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the + * specific language governing permission and limitations under the License. + * + * When distributing Covered Software, include this CDDL Header Notice in each file and include + * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL + * Header, with the fields enclosed by brackets [] replaced by your own identifying + * information: "Portions Copyright [year] [name of copyright owner]". + * + * Copyright 2025 3A Systems, LLC + */ +package com.forgerock.opendj.ldap.extensions; + +import org.forgerock.i18n.LocalizableMessage; +import org.forgerock.opendj.io.ASN1; +import org.forgerock.opendj.io.ASN1Reader; +import org.forgerock.opendj.io.ASN1Writer; +import org.forgerock.opendj.ldap.*; +import org.forgerock.opendj.ldap.controls.Control; +import org.forgerock.opendj.ldap.requests.*; +import org.forgerock.opendj.ldap.responses.AbstractExtendedResultDecoder; +import org.forgerock.opendj.ldap.responses.ExtendedResult; +import org.forgerock.opendj.ldap.responses.ExtendedResultDecoder; +import org.forgerock.util.Reject; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import static com.forgerock.opendj.ldap.CoreMessages.ERR_EXTOP_PASSMOD_CANNOT_DECODE_REQUEST; +import static com.forgerock.opendj.util.StaticUtils.getExceptionMessage; + +/* + An End Transaction Request is an LDAPMessage of CHOICE extendedReq + where the requestName is 1.3.6.1.1.21.3 and the requestValue is + present and contains a BER-encoded txnEndReq. + + txnEndReq ::= SEQUENCE { + commit BOOLEAN DEFAULT TRUE, + identifier OCTET STRING } + + A commit value of TRUE indicates a request to commit the transaction + identified by the identifier. A commit value of FALSE indicates a + request to abort the identified transaction. + */ +public class EndTransactionExtendedRequest extends AbstractExtendedRequest { + + public static final String END_TRANSACTION_REQUEST_OID ="1.3.6.1.1.21.3"; + + @Override + public String getOID() { + return END_TRANSACTION_REQUEST_OID; + } + + @Override + public ExtendedResultDecoder getResultDecoder() { + return RESULT_DECODER; + } + + Boolean commit=true; + public EndTransactionExtendedRequest setCommit(final Boolean commit) { + this.commit = commit; + return this; + } + + String transactionID; + public EndTransactionExtendedRequest setTransactionID(final String transactionID) { + Reject.ifNull(transactionID); + this.transactionID = transactionID; + return this; + } + public boolean isCommit() { + return commit; + } + + public String getTransactionID() { + return transactionID; + } + + @Override + public ByteString getValue() { + Reject.ifNull(transactionID); + final ByteStringBuilder buffer = new ByteStringBuilder(); + final ASN1Writer writer = ASN1.getWriter(buffer); + try { + writer.writeStartSequence(); + if (commit!=null) { + writer.writeBoolean(commit); + } + writer.writeOctetString(transactionID); + writer.writeEndSequence(); + } catch (final IOException ioe) { + throw new RuntimeException(ioe); + } + return buffer.toByteString(); + } + + @Override + public boolean hasValue() { + return true; + } + + private static final EndTransactionExtendedRequest.ResultDecoder RESULT_DECODER = new EndTransactionExtendedRequest.ResultDecoder(); + + private static final class ResultDecoder extends AbstractExtendedResultDecoder { + @Override + public EndTransactionExtendedResult newExtendedErrorResult(final ResultCode resultCode,final String matchedDN, final String diagnosticMessage) { + if (!resultCode.isExceptional()) { + throw new IllegalArgumentException("No response name and value for result code "+ resultCode.intValue()); + } + return EndTransactionExtendedResult.newResult(resultCode).setMatchedDN(matchedDN).setDiagnosticMessage(diagnosticMessage); + } + + /* + txnEndRes ::= SEQUENCE { + messageID MessageID OPTIONAL, + -- msgid associated with non-success resultCode + updatesControls SEQUENCE OF updateControls SEQUENCE { + messageID MessageID, + -- msgid associated with controls + controls Controls + } OPTIONAL + } + */ + @Override + public EndTransactionExtendedResult decodeExtendedResult(final ExtendedResult result,final DecodeOptions options) throws DecodeException { + if (result instanceof EndTransactionExtendedResult) { + return (EndTransactionExtendedResult) result; + } + + final ResultCode resultCode = result.getResultCode(); + final EndTransactionExtendedResult newResult = + EndTransactionExtendedResult.newResult(resultCode) + .setMatchedDN(result.getMatchedDN()) + .setDiagnosticMessage(result.getDiagnosticMessage()); + + final ByteString responseValue = result.getValue(); + if (!resultCode.isExceptional() && responseValue == null) { + throw DecodeException.error(LocalizableMessage.raw("Empty response value")); + } + if (responseValue != null) { + try { + final ASN1Reader reader = ASN1.getReader(responseValue); + if (reader.hasNextElement()) { + reader.readStartSequence(); + if (reader.hasNextElement() && reader.peekType() == ASN1.UNIVERSAL_INTEGER_TYPE) { + newResult.setFailedMessageID(Math.toIntExact(reader.readInteger())); + } else if (reader.hasNextElement() && reader.peekType() == ASN1.UNIVERSAL_SEQUENCE_TYPE) { + reader.readStartSequence(); + while (reader.hasNextElement() && reader.peekType() == ASN1.UNIVERSAL_SEQUENCE_TYPE) { + reader.readStartSequence(); + final long messageId = reader.readInteger(); + final List controls = new ArrayList<>(); + reader.readStartSequence(); + while (reader.hasNextElement() && reader.peekType() == ASN1.UNIVERSAL_OCTET_STRING_TYPE) { + final ByteString controlEncoded = reader.readOctetString(); + //TODO decode Control + } + reader.readEndSequence(); + //newResult.success(messageId, controls.toArray(new Control[]{})); + reader.readEndSequence(); + } + reader.readEndSequence(); + } + reader.readEndSequence(); + } + } catch (final IOException e) { + throw DecodeException.error(LocalizableMessage.raw("Error decoding response value"), e); + } + } + for (final Control control : result.getControls()) { + newResult.addControl(control); + } + return newResult; + } + } +} + diff --git a/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/EndTransactionExtendedResult.java b/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/EndTransactionExtendedResult.java new file mode 100644 index 0000000000..a4ba23ec72 --- /dev/null +++ b/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/EndTransactionExtendedResult.java @@ -0,0 +1,168 @@ +/* + * The contents of this file are subject to the terms of the Common Development and + * Distribution License (the License). You may not use this file except in compliance with the + * License. + * + * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the + * specific language governing permission and limitations under the License. + * + * When distributing Covered Software, include this CDDL Header Notice in each file and include + * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL + * Header, with the fields enclosed by brackets [] replaced by your own identifying + * information: "Portions Copyright [year] [name of copyright owner]". + * + * Copyright 2025 3A Systems, LLC + */ +package com.forgerock.opendj.ldap.extensions; + +import org.forgerock.opendj.io.ASN1; +import org.forgerock.opendj.io.ASN1Writer; +import org.forgerock.opendj.ldap.ByteString; +import org.forgerock.opendj.ldap.ByteStringBuilder; +import org.forgerock.opendj.ldap.ResultCode; +import org.forgerock.opendj.ldap.controls.Control; +import org.forgerock.opendj.ldap.responses.AbstractExtendedResult; +import org.forgerock.util.Reject; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +/* + An End Transaction Response is an LDAPMessage sent in response to a + End Transaction Request. Its response name is absent. The + responseValue when present contains a BER-encoded txnEndRes. + + txnEndRes ::= SEQUENCE { + messageID MessageID OPTIONAL, + -- msgid associated with non-success resultCode + updatesControls SEQUENCE OF updateControls SEQUENCE { + messageID MessageID, + -- msgid associated with controls + controls Controls + } OPTIONAL + } + -- where MessageID and Controls are as specified in RFC 4511 + + The txnEndRes.messageID provides the message id of the update request + associated with a non-success response. txnEndRes.messageID is + absent when resultCode of the End Transaction Response is success + + The txnEndRes.updatesControls provides a facility for returning + response controls that normally (i.e., in the absence of + transactions) would be returned in an update response. The + updateControls.messageID provides the message id of the update + request associated with the response controls provided in + updateControls.controls. + + The txnEndRes.updatesControls is absent when there are no update + response controls to return. + + If both txnEndRes.messageID and txnEndRes.updatesControl are absent, + the responseValue of the End Transaction Response is absent. + */ +public class EndTransactionExtendedResult extends AbstractExtendedResult { + @Override + public String getOID() { + return EndTransactionExtendedRequest.END_TRANSACTION_REQUEST_OID; + } + + private EndTransactionExtendedResult(final ResultCode resultCode) { + super(resultCode); + } + + public static EndTransactionExtendedResult newResult(final ResultCode resultCode) { + Reject.ifNull(resultCode); + return new EndTransactionExtendedResult(resultCode); + } + + // The message ID for the operation that failed, if applicable. + Integer failedOpMessageID=null; + + public EndTransactionExtendedResult setFailedMessageID(final Integer failedOpMessageID) { + Reject.ifNull(failedOpMessageID); + this.failedOpMessageID = failedOpMessageID; + return this; + } + + // A mapping of the response controls for the operations performed as part of + // the transaction. + Map> opResponseControls= new TreeMap<>(); + + public EndTransactionExtendedResult success(Integer messageID, List responses) { + Reject.ifNull(messageID); + Reject.ifNull(responses); + opResponseControls.put(messageID,responses); + return this; + } + + /* + txnEndRes ::= SEQUENCE { + messageID MessageID OPTIONAL, + -- msgid associated with non-success resultCode + updatesControls SEQUENCE OF updateControls SEQUENCE { + messageID MessageID, + -- msgid associated with controls + controls Controls + } OPTIONAL + } + */ + @Override + public ByteString getValue() { + final ByteStringBuilder buffer = new ByteStringBuilder(); + final ASN1Writer writer = ASN1.getWriter(buffer); + try { + if (failedOpMessageID!=null || (opResponseControls!=null && !opResponseControls.isEmpty()) ) { + writer.writeStartSequence(); + if (failedOpMessageID != null) { + writer.writeInteger(failedOpMessageID); + } + if (opResponseControls != null && !opResponseControls.isEmpty()) { + writer.writeStartSequence(); + for (Map.Entry> entry : opResponseControls.entrySet()) { + writer.writeStartSequence(); + writer.writeInteger(entry.getKey()); + writer.writeStartSequence(); + for (Control control : entry.getValue()) { + writer.writeOctetString(control.getValue()); + } + writer.writeEndSequence(); + writer.writeEndSequence(); + } + writer.writeEndSequence(); + } + writer.writeEndSequence(); + } + } catch (final IOException ioe) { + throw new RuntimeException(ioe); + } + return buffer.toByteString(); + } + + @Override + public boolean hasValue() { + return true; + } + + @Override + public String toString() { + return "EndTransactionExtendedResult(resultCode=" + + getResultCode() + + ", matchedDN=" + + getMatchedDN() + + ", diagnosticMessage=" + + getDiagnosticMessage() + + ", referrals=" + + getReferralURIs() + + ", responseName=" + + getOID() + + ", failedOpMessageID=" + + failedOpMessageID + + ", opResponseControls=" + + opResponseControls + + ", controls=" + + getControls() + + ")"; + } +} \ No newline at end of file diff --git a/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/StartTransactionExtendedRequest.java b/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/StartTransactionExtendedRequest.java new file mode 100644 index 0000000000..647063dcd5 --- /dev/null +++ b/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/StartTransactionExtendedRequest.java @@ -0,0 +1,101 @@ +/* + * The contents of this file are subject to the terms of the Common Development and + * Distribution License (the License). You may not use this file except in compliance with the + * License. + * + * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the + * specific language governing permission and limitations under the License. + * + * When distributing Covered Software, include this CDDL Header Notice in each file and include + * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL + * Header, with the fields enclosed by brackets [] replaced by your own identifying + * information: "Portions Copyright [year] [name of copyright owner]". + * + * Copyright 2025 3A Systems, LLC + */ +package com.forgerock.opendj.ldap.extensions; + +import org.forgerock.i18n.LocalizableMessage; +import org.forgerock.opendj.io.ASN1; +import org.forgerock.opendj.io.ASN1Reader; +import org.forgerock.opendj.ldap.ByteString; +import org.forgerock.opendj.ldap.DecodeException; +import org.forgerock.opendj.ldap.DecodeOptions; +import org.forgerock.opendj.ldap.ResultCode; +import org.forgerock.opendj.ldap.controls.Control; +import org.forgerock.opendj.ldap.requests.AbstractExtendedRequest; +import org.forgerock.opendj.ldap.responses.AbstractExtendedResultDecoder; +import org.forgerock.opendj.ldap.responses.ExtendedResult; +import org.forgerock.opendj.ldap.responses.ExtendedResultDecoder; + +import java.io.IOException; + +/* + A Start Transaction Request is an LDAPMessage of CHOICE extendedReq + where the requestName is 1.3.6.1.1.21.1 and the requestValue is + absent. + */ +public class StartTransactionExtendedRequest extends AbstractExtendedRequest { + public static final String START_TRANSACTION_REQUEST_OID ="1.3.6.1.1.21.1"; + + @Override + public String getOID() { + return START_TRANSACTION_REQUEST_OID; + } + @Override + public ExtendedResultDecoder getResultDecoder() { + return RESULT_DECODER; + } + + @Override + public ByteString getValue() { + return null; + } + + @Override + public boolean hasValue() { + return false; + } + + private static final StartTransactionExtendedRequest.ResultDecoder RESULT_DECODER = new StartTransactionExtendedRequest.ResultDecoder(); + + private static final class ResultDecoder extends AbstractExtendedResultDecoder { + @Override + public StartTransactionExtendedResult newExtendedErrorResult(final ResultCode resultCode,final String matchedDN, final String diagnosticMessage) { + if (!resultCode.isExceptional()) { + throw new IllegalArgumentException("No response name and value for result code "+ resultCode.intValue()); + } + return StartTransactionExtendedResult.newResult(resultCode).setMatchedDN(matchedDN).setDiagnosticMessage(diagnosticMessage); + } + + @Override + public StartTransactionExtendedResult decodeExtendedResult(final ExtendedResult result,final DecodeOptions options) throws DecodeException { + if (result instanceof StartTransactionExtendedResult) { + return (StartTransactionExtendedResult) result; + } + + final ResultCode resultCode = result.getResultCode(); + final StartTransactionExtendedResult newResult = + StartTransactionExtendedResult.newResult(resultCode) + .setMatchedDN(result.getMatchedDN()) + .setDiagnosticMessage(result.getDiagnosticMessage()); + + final ByteString responseValue = result.getValue(); + if (!resultCode.isExceptional() && responseValue == null) { + throw DecodeException.error(LocalizableMessage.raw("Empty response value")); + } + if (responseValue != null) { + try { + final ASN1Reader reader = ASN1.getReader(responseValue); + newResult.setTransactionID(reader.readOctetStringAsString()); + } catch (final IOException e) { + throw DecodeException.error(LocalizableMessage.raw("Error decoding response value"), e); + } + } + for (final Control control : result.getControls()) { + newResult.addControl(control); + } + return newResult; + } + } +} diff --git a/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/StartTransactionExtendedResult.java b/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/StartTransactionExtendedResult.java new file mode 100644 index 0000000000..40dbf04d23 --- /dev/null +++ b/opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/StartTransactionExtendedResult.java @@ -0,0 +1,96 @@ +/* + * The contents of this file are subject to the terms of the Common Development and + * Distribution License (the License). You may not use this file except in compliance with the + * License. + * + * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the + * specific language governing permission and limitations under the License. + * + * When distributing Covered Software, include this CDDL Header Notice in each file and include + * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL + * Header, with the fields enclosed by brackets [] replaced by your own identifying + * information: "Portions Copyright [year] [name of copyright owner]". + * + * Copyright 2025 3A Systems, LLC + */ +package com.forgerock.opendj.ldap.extensions; + +import org.forgerock.opendj.io.ASN1; +import org.forgerock.opendj.io.ASN1Writer; +import org.forgerock.opendj.ldap.ByteString; +import org.forgerock.opendj.ldap.ByteStringBuilder; +import org.forgerock.opendj.ldap.ResultCode; +import org.forgerock.opendj.ldap.responses.AbstractExtendedResult; +import org.forgerock.util.Reject; + +import java.io.IOException; + +/* + A Start Transaction Response is an LDAPMessage of CHOICE extendedRes + sent in response to a Start Transaction Request. Its responseName is + absent. When the resultCode is success (0), responseValue is present + and contains a transaction identifier. Otherwise, the responseValue + is absent. + */ +public class StartTransactionExtendedResult extends AbstractExtendedResult { + @Override + public String getOID() { + return StartTransactionExtendedRequest.START_TRANSACTION_REQUEST_OID; + } + + private StartTransactionExtendedResult(final ResultCode resultCode) { + super(resultCode); + } + + public static StartTransactionExtendedResult newResult(final ResultCode resultCode) { + Reject.ifNull(resultCode); + return new StartTransactionExtendedResult(resultCode); + } + + private String transactionID = null; + + public StartTransactionExtendedResult setTransactionID(final String transactionID) { + this.transactionID = transactionID; + return this; + } + + public String getTransactionID() { + return transactionID; + } + + @Override + public ByteString getValue() { + final ByteStringBuilder buffer = new ByteStringBuilder(); + final ASN1Writer writer = ASN1.getWriter(buffer); + try { + writer.writeOctetString(transactionID); + } catch (final IOException ioe) { + throw new RuntimeException(ioe); + } + return buffer.toByteString(); + } + + @Override + public boolean hasValue() { + return true; + } + + @Override + public String toString() { + return "StartTransactionExtendedResult(resultCode=" + + getResultCode() + + ", matchedDN=" + + getMatchedDN() + + ", diagnosticMessage=" + + getDiagnosticMessage() + + ", referrals=" + + getReferralURIs() + + ", responseName=" + + getOID() + + ", transactionID=" + + transactionID + + ", controls=" + + getControls() + + ")"; + } +} diff --git a/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/TransactionSpecificationRequestControl.java b/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/TransactionSpecificationRequestControl.java new file mode 100644 index 0000000000..2d4f589707 --- /dev/null +++ b/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/TransactionSpecificationRequestControl.java @@ -0,0 +1,82 @@ +/* + * The contents of this file are subject to the terms of the Common Development and + * Distribution License (the License). You may not use this file except in compliance with the + * License. + * + * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the + * specific language governing permission and limitations under the License. + * + * When distributing Covered Software, include this CDDL Header Notice in each file and include + * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL + * Header, with the fields enclosed by brackets [] replaced by your own identifying + * information: "Portions Copyright [year] [name of copyright owner]". + * + * Copyright 2025 3A Systems,LLC. + */ +package org.forgerock.opendj.ldap.controls; + +import org.forgerock.opendj.ldap.ByteString; +import org.forgerock.opendj.ldap.DecodeException; +import org.forgerock.opendj.ldap.DecodeOptions; +import org.forgerock.util.Reject; + +import static com.forgerock.opendj.ldap.CoreMessages.ERR_TRANSACTION_ID_CONTROL_BAD_OID; +import static com.forgerock.opendj.ldap.CoreMessages.ERR_TRANSACTION_ID_CONTROL_DECODE_NULL; + +public class TransactionSpecificationRequestControl implements Control{ + + public final static String OID="1.3.6.1.1.21.2"; + + final String transactionId; + public TransactionSpecificationRequestControl(String transactionId) { + Reject.ifNull(transactionId); + this.transactionId = transactionId; + } + + @Override + public String getOID() { + return OID; + } + + @Override + public ByteString getValue() { + return ByteString.valueOfUtf8(transactionId); + } + + @Override + public boolean hasValue() { + return true; + } + + @Override + public boolean isCritical() { + return true; + } + + public static final ControlDecoder DECODER = new ControlDecoder() { + @Override + public TransactionSpecificationRequestControl decodeControl(final Control control, final DecodeOptions options) + throws DecodeException { + Reject.ifNull(control); + + if (control instanceof TransactionSpecificationRequestControl) { + return (TransactionSpecificationRequestControl) control; + } + + if (!control.getOID().equals(OID)) { + throw DecodeException.error(ERR_TRANSACTION_ID_CONTROL_BAD_OID.get(control.getOID(), OID)); + } + + if (!control.hasValue()) { + throw DecodeException.error(ERR_TRANSACTION_ID_CONTROL_DECODE_NULL.get()); + } + + return new TransactionSpecificationRequestControl(control.getValue().toString()); + } + + @Override + public String getOID() { + return OID; + } + }; +} diff --git a/opendj-doc-generated-ref/src/main/asciidoc/reference/appendix-standards.adoc b/opendj-doc-generated-ref/src/main/asciidoc/reference/appendix-standards.adoc index 5d3707fb83..926f3b96a6 100644 --- a/opendj-doc-generated-ref/src/main/asciidoc/reference/appendix-standards.adoc +++ b/opendj-doc-generated-ref/src/main/asciidoc/reference/appendix-standards.adoc @@ -24,7 +24,7 @@ [#appendix-standards] == Standards, RFCs, & Internet-Drafts -OpenDJ 3.5 software implements the following RFCs, Internet-Drafts, and standards: +OpenDJ software implements the following RFCs, Internet-Drafts, and standards: -- [#rfc1274] @@ -396,6 +396,13 @@ link:http://tools.ietf.org/html/rfc5020[RFC 5020: The Lightweight Directory Acce + Describes the Lightweight Directory Access Protocol (LDAP) / X.500 'entryDN' operational attribute, that provides a copy of the entry's distinguished name for use in attribute value assertions. +[#rfc5805] +link:http://tools.ietf.org/html/rfc5805[RFC 5805: Lightweight Directory Access Protocol (LDAP) Transactions, window=\_top]:: ++ +Lightweight Directory Access Protocol (LDAP) update operations, such as Add, Delete, and Modify operations, have atomic, consistency, +isolation, durability (ACID) properties. Each of these update operations act upon an entry. It is often desirable to update two or +more entries in a single unit of interaction, a transaction. + [#fips180-1] link:http://www.itl.nist.gov/fipspubs/fip180-1.htm[FIPS 180-1: Secure Hash Standard (SHA-1), window=\_top]:: + diff --git a/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/EndTransactionExtendedOperationHandlerConfiguration.xml b/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/EndTransactionExtendedOperationHandlerConfiguration.xml new file mode 100644 index 0000000000..36317510d1 --- /dev/null +++ b/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/EndTransactionExtendedOperationHandlerConfiguration.xml @@ -0,0 +1,75 @@ + + + + + The + + An End Transaction Request is an LDAPMessage of CHOICE extendedReq + where the requestName is 1.3.6.1.1.21.3 and the requestValue is + present and contains a BER-encoded txnEndReq. + + txnEndReq ::= SEQUENCE { + commit BOOLEAN DEFAULT TRUE, + identifier OCTET STRING } + + A commit value of TRUE indicates a request to commit the transaction + identified by the identifier. A commit value of FALSE indicates a + request to abort the identified transaction. + + An End Transaction Response is an LDAPMessage sent in response to a + End Transaction Request. Its response name is absent. The + responseValue when present contains a BER-encoded txnEndRes. + + txnEndRes ::= SEQUENCE { + messageID MessageID OPTIONAL, + -- msgid associated with non-success resultCode + updatesControls SEQUENCE OF updateControls SEQUENCE { + messageID MessageID, + -- msgid associated with controls + controls Controls + } OPTIONAL + } + -- where MessageID and Controls are as specified in RFC 4511 + + The txnEndRes.messageID provides the message id of the update request + associated with a non-success response. txnEndRes.messageID is + absent when resultCode of the End Transaction Response is success + (0). + + + + + ds-cfg-end-transaction-extended-operation-handler + + ds-cfg-extended-operation-handler + + + + + + + org.opends.server.extensions.EndTransactionExtendedOperation + + + + + diff --git a/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/StartTransactionExtendedOperationHandlerConfiguration.xml b/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/StartTransactionExtendedOperationHandlerConfiguration.xml new file mode 100644 index 0000000000..e70290a8b3 --- /dev/null +++ b/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/StartTransactionExtendedOperationHandlerConfiguration.xml @@ -0,0 +1,55 @@ + + + + + The + + A client wishing to perform a sequence of directory updates as a + transaction issues a Start Transaction Request. A server that is + willing and able to support transactions responds to this request + with a Start Transaction Response providing a transaction identifier + and with a resultCode of success (0). Otherwise, the server responds + with a Start Transaction Response with a resultCode other than + success indicating the nature of the failure. + + The transaction identifier provided upon successful start of a + transaction is used in subsequent protocol messages to identify this + transaction. + + + + + ds-cfg-start-transaction-extended-operation-handler + + ds-cfg-extended-operation-handler + + + + + + + org.opends.server.extensions.StartTransactionExtendedOperation + + + + + diff --git a/opendj-server-legacy/resource/config/config.ldif b/opendj-server-legacy/resource/config/config.ldif index f836b18fc3..0683e93851 100644 --- a/opendj-server-legacy/resource/config/config.ldif +++ b/opendj-server-legacy/resource/config/config.ldif @@ -14,6 +14,7 @@ # Portions Copyright 2012-2014 Manuel Gaupp # Portions Copyright 2010-2016 ForgeRock AS. # Portions copyright 2015 Edan Idzerda +# Portions Copyright 2025 3A Systems, LLC. # This file contains the primary Directory Server configuration. It must not # be directly edited while the server is online. The server configuration @@ -603,6 +604,22 @@ cn: Who Am I ds-cfg-java-class: org.opends.server.extensions.WhoAmIExtendedOperation ds-cfg-enabled: true +dn: cn=Start Transaction,cn=Extended Operations,cn=config +objectClass: top +objectClass: ds-cfg-extended-operation-handler +objectClass: ds-cfg-start-transaction-extended-operation-handler +cn: Start Transaction +ds-cfg-java-class: org.opends.server.extensions.StartTransactionExtendedOperation +ds-cfg-enabled: true + +dn: cn=End Transaction,cn=Extended Operations,cn=config +objectClass: top +objectClass: ds-cfg-extended-operation-handler +objectClass: ds-cfg-end-transaction-extended-operation-handler +cn: End Transaction +ds-cfg-java-class: org.opends.server.extensions.EndTransactionExtendedOperation +ds-cfg-enabled: true + dn: cn=Group Implementations,cn=config objectClass: top objectClass: ds-cfg-branch diff --git a/opendj-server-legacy/resource/schema/02-config.ldif b/opendj-server-legacy/resource/schema/02-config.ldif index 697a1aed15..b59a3a2f53 100644 --- a/opendj-server-legacy/resource/schema/02-config.ldif +++ b/opendj-server-legacy/resource/schema/02-config.ldif @@ -15,7 +15,7 @@ # Portions Copyright 2011 profiq, s.r.o. # Portions Copyright 2012 Manuel Gaupp # Portions copyright 2015 Edan Idzerda -# Portions copyright 2023-2024 3A Systems LLC +# Portions copyright 2023-2025 3A Systems LLC # This file contains the attribute type and objectclass definitions for use # with the Directory Server configuration. @@ -6257,4 +6257,13 @@ objectClasses: ( 1.3.6.1.4.1.36733.2.1.2.105 STRUCTURAL MAY ds-cfg-pbkdf2-iterations X-ORIGIN 'OpenDJ Directory Server' ) - \ No newline at end of file +objectClasses: ( 1.3.6.1.4.1.60142.1.2.1 + NAME 'ds-cfg-start-transaction-extended-operation-handler' + SUP ds-cfg-extended-operation-handler + STRUCTURAL + X-ORIGIN 'OpenDS Directory Server' ) +objectClasses: ( 1.3.6.1.4.1.60142.1.2.2 + NAME 'ds-cfg-end-transaction-extended-operation-handler' + SUP ds-cfg-extended-operation-handler + STRUCTURAL + X-ORIGIN 'OpenDS Directory Server' ) \ No newline at end of file diff --git a/opendj-server-legacy/src/main/java/org/opends/server/api/ClientConnection.java b/opendj-server-legacy/src/main/java/org/opends/server/api/ClientConnection.java index 7487b3de12..a891fee116 100644 --- a/opendj-server-legacy/src/main/java/org/opends/server/api/ClientConnection.java +++ b/opendj-server-legacy/src/main/java/org/opends/server/api/ClientConnection.java @@ -13,6 +13,7 @@ * * Copyright 2006-2009 Sun Microsystems, Inc. * Portions Copyright 2011-2016 ForgeRock AS. + * Portions Copyright 2025 3A Systems, LLC. */ package org.opends.server.api; @@ -20,11 +21,8 @@ import java.nio.channels.ByteChannel; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicBoolean; @@ -53,6 +51,7 @@ import org.opends.server.types.Privilege; import org.opends.server.types.SearchResultEntry; import org.opends.server.types.SearchResultReference; +import org.opends.server.types.operation.RollbackOperation; import org.opends.server.util.TimeThread; import static org.opends.messages.CoreMessages.*; @@ -1576,4 +1575,47 @@ public boolean isInnerConnection() { return getConnectionID() < 0; } + + public class Transaction { + final String transactionId=UUID.randomUUID().toString().toLowerCase(); + + public Transaction() { + transactions.put(getTransactionId(),this); + } + + public String getTransactionId() { + return transactionId; + } + + final Queue waiting=new LinkedList<>(); + public void add(Operation operation) { + waiting.add(operation); + } + + public Queue getWaiting() { + return waiting; + } + + public void clear() { + transactions.remove(getTransactionId()); + } + final Deque completed =new ArrayDeque<>(); + public void success(RollbackOperation operation) { + completed.add(operation); + } + + public Deque getCompleted() { + return completed; + } + } + + Map transactions=new ConcurrentHashMap<>(); + + public Transaction startTransaction() { + return new Transaction(); + } + + public Transaction getTransaction(String id) { + return transactions.get(id); + } } diff --git a/opendj-server-legacy/src/main/java/org/opends/server/extensions/EndTransactionExtendedOperation.java b/opendj-server-legacy/src/main/java/org/opends/server/extensions/EndTransactionExtendedOperation.java new file mode 100644 index 0000000000..9ebb554450 --- /dev/null +++ b/opendj-server-legacy/src/main/java/org/opends/server/extensions/EndTransactionExtendedOperation.java @@ -0,0 +1,131 @@ +/* + * The contents of this file are subject to the terms of the Common Development and + * Distribution License (the License). You may not use this file except in compliance with the + * License. + * + * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the + * specific language governing permission and limitations under the License. + * + * When distributing Covered Software, include this CDDL Header Notice in each file and include + * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL + * Header, with the fields enclosed by brackets [] replaced by your own identifying + * information: "Portions Copyright [year] [name of copyright owner]". + * + * Copyright 2025 3A Systems, LLC + */ +package org.opends.server.extensions; + +import com.forgerock.opendj.ldap.extensions.EndTransactionExtendedRequest; +import com.forgerock.opendj.ldap.extensions.EndTransactionExtendedResult; +import org.forgerock.i18n.LocalizableMessage; +import org.forgerock.i18n.slf4j.LocalizedLogger; +import org.forgerock.opendj.config.server.ConfigException; +import org.forgerock.opendj.io.ASN1; +import org.forgerock.opendj.io.ASN1Reader; +import org.forgerock.opendj.ldap.DecodeException; +import org.forgerock.opendj.ldap.ResultCode; +import org.forgerock.opendj.server.config.server.EndTransactionExtendedOperationHandlerCfg; +import org.opends.server.api.ClientConnection; +import org.opends.server.api.ExtendedOperationHandler; +import org.opends.server.core.ExtendedOperation; +import org.opends.server.types.DirectoryException; +import org.opends.server.types.InitializationException; +import org.opends.server.types.Operation; +import org.opends.server.types.operation.RollbackOperation; + +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.LinkedList; +import java.util.Queue; + +import static com.forgerock.opendj.ldap.CoreMessages.ERR_EXTOP_PASSMOD_CANNOT_DECODE_REQUEST; +import static com.forgerock.opendj.util.StaticUtils.getExceptionMessage; + + +public class EndTransactionExtendedOperation extends ExtendedOperationHandler +{ + private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); + + public EndTransactionExtendedOperation() + { + super(); + } + + @Override + public void initializeExtendedOperationHandler(EndTransactionExtendedOperationHandlerCfg config) throws ConfigException, InitializationException + { + super.initializeExtendedOperationHandler(config); + } + + @Override + public void processExtendedOperation(ExtendedOperation operation) + { + final EndTransactionExtendedRequest request =new EndTransactionExtendedRequest(); + if (operation.getRequestValue()!= null) { + final ASN1Reader reader = ASN1.getReader(operation.getRequestValue()); + try { + reader.readStartSequence(); + if (reader.hasNextElement()&& (reader.peekType() == ASN1.UNIVERSAL_BOOLEAN_TYPE)) { + request.setCommit(reader.readBoolean()); + } + request.setTransactionID(reader.readOctetStringAsString()); + reader.readEndSequence(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + final ClientConnection.Transaction trn=operation.getClientConnection().getTransaction(request.getTransactionID()); + if (trn==null) { + operation.setResultCode(ResultCode.CANCELLED); + operation.appendErrorMessage(LocalizableMessage.raw("unknown transactionId="+request.getTransactionID())); + return; + } + + final EndTransactionExtendedResult res=EndTransactionExtendedResult.newResult(ResultCode.SUCCESS); + operation.setResultCode(res.getResultCode()); + Operation currentOperation=null; + try { + while((currentOperation=trn.getWaiting().poll())!=null) { + if (request.isCommit()) { + currentOperation.run(); + if (!ResultCode.SUCCESS.equals(currentOperation.getResultCode())) { + throw new InterruptedException(); + } + currentOperation.operationCompleted(); + //res.success(currentOperation.getMessageID(),currentOperation.getResponseControls()); + } + } + }catch (Throwable e){ + res.setFailedMessageID(currentOperation.getMessageID()); + operation.setResultCode(currentOperation.getResultCode()); + operation.setErrorMessage(currentOperation.getErrorMessage()); + //rollback + RollbackOperation cancelOperation=null; + while((cancelOperation=trn.getCompleted().pollLast())!=null) { + try { + cancelOperation.rollback(); + }catch (Throwable e2){ + throw new RuntimeException("rollback error",e2); + } + } + }finally { + trn.clear(); + } + operation.setResponseOID(res.getOID()); + operation.setResponseValue(res.getValue()); + } + + @Override + public String getExtendedOperationOID() + { + return EndTransactionExtendedRequest.END_TRANSACTION_REQUEST_OID; + } + + @Override + public String getExtendedOperationName() + { + return "End Transaction"; + } +} diff --git a/opendj-server-legacy/src/main/java/org/opends/server/extensions/StartTransactionExtendedOperation.java b/opendj-server-legacy/src/main/java/org/opends/server/extensions/StartTransactionExtendedOperation.java new file mode 100644 index 0000000000..b7fb0a3549 --- /dev/null +++ b/opendj-server-legacy/src/main/java/org/opends/server/extensions/StartTransactionExtendedOperation.java @@ -0,0 +1,66 @@ +/* + * The contents of this file are subject to the terms of the Common Development and + * Distribution License (the License). You may not use this file except in compliance with the + * License. + * + * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the + * specific language governing permission and limitations under the License. + * + * When distributing Covered Software, include this CDDL Header Notice in each file and include + * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL + * Header, with the fields enclosed by brackets [] replaced by your own identifying + * information: "Portions Copyright [year] [name of copyright owner]". + * + * Copyright 2025 3A Systems, LLC + */ +package org.opends.server.extensions; + +import com.forgerock.opendj.ldap.extensions.StartTransactionExtendedRequest; +import com.forgerock.opendj.ldap.extensions.StartTransactionExtendedResult; +import org.forgerock.i18n.slf4j.LocalizedLogger; +import org.forgerock.opendj.config.server.ConfigException; +import org.forgerock.opendj.ldap.ResultCode; +import org.forgerock.opendj.server.config.server.StartTransactionExtendedOperationHandlerCfg; +import org.opends.server.api.ExtendedOperationHandler; +import org.opends.server.core.ExtendedOperation; +import org.opends.server.types.InitializationException; + +public class StartTransactionExtendedOperation extends ExtendedOperationHandler +{ + private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); + + public StartTransactionExtendedOperation() + { + super(); + } + + @Override + public void initializeExtendedOperationHandler(StartTransactionExtendedOperationHandlerCfg config) throws ConfigException, InitializationException + { + super.initializeExtendedOperationHandler(config); + } + + @Override + public void processExtendedOperation(ExtendedOperation operation) + { + final StartTransactionExtendedResult res=StartTransactionExtendedResult + .newResult(ResultCode.SUCCESS) + .setTransactionID(operation.getClientConnection().startTransaction().getTransactionId()); + + operation.setResponseOID(res.getOID()); + operation.setResponseValue(res.getValue()); + operation.setResultCode(res.getResultCode()); + } + + @Override + public String getExtendedOperationOID() + { + return StartTransactionExtendedRequest.START_TRANSACTION_REQUEST_OID; + } + + @Override + public String getExtendedOperationName() + { + return "Start Transaction"; + } +} diff --git a/opendj-server-legacy/src/main/java/org/opends/server/extensions/TraditionalWorkerThread.java b/opendj-server-legacy/src/main/java/org/opends/server/extensions/TraditionalWorkerThread.java index d88bdcaba9..1ceae70400 100644 --- a/opendj-server-legacy/src/main/java/org/opends/server/extensions/TraditionalWorkerThread.java +++ b/opendj-server-legacy/src/main/java/org/opends/server/extensions/TraditionalWorkerThread.java @@ -13,15 +13,19 @@ * * Copyright 2006-2010 Sun Microsystems, Inc. * Portions Copyright 2011-2016 ForgeRock AS. + * Portions Copyright 2025 3A Systems, LLC. */ package org.opends.server.extensions; import java.util.Map; import org.forgerock.i18n.LocalizableMessage; +import org.forgerock.opendj.ldap.ResultCode; +import org.opends.server.api.ClientConnection; import org.opends.server.api.DirectoryThread; import org.opends.server.core.DirectoryServer; import org.forgerock.i18n.slf4j.LocalizedLogger; +import org.opends.server.types.AbstractOperation; import org.opends.server.types.CancelRequest; import org.opends.server.types.DisconnectReason; import org.opends.server.types.Operation; @@ -145,8 +149,29 @@ public void run() { // The operation is not null, so process it. Make sure that when // processing is complete. - operation.run(); - operation.operationCompleted(); + + //check has transactionId control + ClientConnection.Transaction transaction=null; + if (operation instanceof AbstractOperation) { + String transactionId = ((AbstractOperation) operation).getTransactionId(); + if (transactionId!=null){ + transaction=operation.getClientConnection().getTransaction(transactionId); + if (transaction==null){ //unknown transactionId + operation.setResultCode(ResultCode.CANCELLED); + operation.appendErrorMessage(LocalizableMessage.raw("unknown transactionId="+transactionId)); + operation.getClientConnection().sendResponse(operation); + continue; + } + } + } + if (transaction==null) { //run + operation.run(); + operation.operationCompleted(); + }else { //suspend for commit + transaction.add(operation); + operation.setResultCode(ResultCode.SUCCESS); + operation.getClientConnection().sendResponse(operation); + } } } catch (Throwable t) diff --git a/opendj-server-legacy/src/main/java/org/opends/server/types/AbstractOperation.java b/opendj-server-legacy/src/main/java/org/opends/server/types/AbstractOperation.java index 61ee9f2a2d..5f5c67c676 100644 --- a/opendj-server-legacy/src/main/java/org/opends/server/types/AbstractOperation.java +++ b/opendj-server-legacy/src/main/java/org/opends/server/types/AbstractOperation.java @@ -13,6 +13,7 @@ * * Copyright 2006-2010 Sun Microsystems, Inc. * Portions Copyright 2011-2016 ForgeRock AS. + * Portions Copyright 2023-2025 3A Systems, LLC. */ package org.opends.server.types; @@ -29,6 +30,7 @@ import org.forgerock.i18n.slf4j.LocalizedLogger; import org.forgerock.opendj.ldap.DN; import org.forgerock.opendj.ldap.ResultCode; +import org.forgerock.opendj.ldap.controls.TransactionSpecificationRequestControl; import org.forgerock.util.Reject; import org.opends.server.api.ClientConnection; import org.opends.server.api.plugin.PluginResult.OperationResult; @@ -721,4 +723,16 @@ public static boolean processOperationResult(Operation op, OperationResult opRes } return true; } + + public String getTransactionId() { + for (Control control : getRequestControls()) { + if (control.getOID().equals(TransactionSpecificationRequestControl.OID)) { + if ((control instanceof LDAPControl) && ((LDAPControl)control).getValue()!=null){ + return ((LDAPControl) control).getValue().toString(); + } + } + } + return null; + } + } diff --git a/opendj-server-legacy/src/main/java/org/opends/server/types/operation/RollbackOperation.java b/opendj-server-legacy/src/main/java/org/opends/server/types/operation/RollbackOperation.java new file mode 100644 index 0000000000..cdeb15199b --- /dev/null +++ b/opendj-server-legacy/src/main/java/org/opends/server/types/operation/RollbackOperation.java @@ -0,0 +1,26 @@ +/* + * The contents of this file are subject to the terms of the Common Development and + * Distribution License (the License). You may not use this file except in compliance with the + * License. + * + * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the + * specific language governing permission and limitations under the License. + * + * When distributing Covered Software, include this CDDL Header Notice in each file and include + * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL + * Header, with the fields enclosed by brackets [] replaced by your own identifying + * information: "Portions Copyright [year] [name of copyright owner]". + * + * Copyright 2025 3A Systems,LLC. + */ +package org.opends.server.types.operation; + +import org.opends.server.api.ClientConnection; +import org.opends.server.types.CanceledOperationException; +import org.opends.server.types.DirectoryException; + +public interface RollbackOperation +{ + void rollback() throws CanceledOperationException, DirectoryException; +} + diff --git a/opendj-server-legacy/src/main/java/org/opends/server/workflowelement/localbackend/LocalBackendAddOperation.java b/opendj-server-legacy/src/main/java/org/opends/server/workflowelement/localbackend/LocalBackendAddOperation.java index 1b86927170..03a1e3bd1d 100644 --- a/opendj-server-legacy/src/main/java/org/opends/server/workflowelement/localbackend/LocalBackendAddOperation.java +++ b/opendj-server-legacy/src/main/java/org/opends/server/workflowelement/localbackend/LocalBackendAddOperation.java @@ -13,7 +13,7 @@ * * Copyright 2008-2010 Sun Microsystems, Inc. * Portions Copyright 2011-2016 ForgeRock AS. - * Portions copyright 2024 3A Systems,LLC. + * Portions Copyright 2024-2025 3A Systems,LLC. */ package org.opends.server.workflowelement.localbackend; @@ -39,6 +39,7 @@ import org.forgerock.opendj.ldap.DN; import org.forgerock.opendj.ldap.ResultCode; import org.forgerock.opendj.ldap.controls.RelaxRulesControl; +import org.forgerock.opendj.ldap.controls.TransactionSpecificationRequestControl; import org.forgerock.opendj.ldap.schema.AttributeType; import org.forgerock.opendj.ldap.schema.ObjectClass; import org.forgerock.opendj.ldap.schema.Syntax; @@ -61,6 +62,7 @@ import org.opends.server.core.PasswordPolicy; import org.opends.server.core.PersistentSearch; import org.opends.server.core.ServerContext; +import org.opends.server.protocols.ldap.LDAPControl; import org.opends.server.schema.AuthPasswordSyntax; import org.opends.server.schema.UserPasswordSyntax; import org.opends.server.types.Attribute; @@ -73,10 +75,7 @@ import org.opends.server.types.LockManager.DNLock; import org.opends.server.types.Privilege; import org.opends.server.types.SearchFilter; -import org.opends.server.types.operation.PostOperationAddOperation; -import org.opends.server.types.operation.PostResponseAddOperation; -import org.opends.server.types.operation.PostSynchronizationAddOperation; -import org.opends.server.types.operation.PreOperationAddOperation; +import org.opends.server.types.operation.*; import org.opends.server.util.TimeThread; /** @@ -86,7 +85,7 @@ public class LocalBackendAddOperation extends AddOperationWrapper implements PreOperationAddOperation, PostOperationAddOperation, - PostResponseAddOperation, PostSynchronizationAddOperation + PostResponseAddOperation, PostSynchronizationAddOperation,RollbackOperation { private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); @@ -470,6 +469,9 @@ private void processAdd(ClientConnection clientConnection, } backend.addEntry(entry, this); + if (trx!=null) { + trx.success(this); + } } LocalBackendWorkflowElement.addPostReadResponse(this, postReadRequest, @@ -496,7 +498,10 @@ private void processAdd(ClientConnection clientConnection, } } - + @Override + public void rollback() throws CanceledOperationException, DirectoryException { + backend.deleteEntry(entryDN,null); + } private void processSynchPostOperationPlugins() { @@ -968,6 +973,10 @@ else if (RelaxRulesControl.OID.equals(oid)) { RelaxRulesControlRequested = true; } + else if (TransactionSpecificationRequestControl.OID.equals(oid)) + { + trx=getClientConnection().getTransaction(((LDAPControl)c).getValue().toString()); + } else if (c.isCritical() && !backend.supportsControl(oid)) { throw newDirectoryException(entryDN, ResultCode.UNAVAILABLE_CRITICAL_EXTENSION, @@ -975,9 +984,11 @@ else if (c.isCritical() && !backend.supportsControl(oid)) } } } + ClientConnection.Transaction trx=null; private AccessControlHandler getAccessControlHandler() { return AccessControlConfigManager.getInstance().getAccessControlHandler(); } + } diff --git a/opendj-server-legacy/src/main/java/org/opends/server/workflowelement/localbackend/LocalBackendDeleteOperation.java b/opendj-server-legacy/src/main/java/org/opends/server/workflowelement/localbackend/LocalBackendDeleteOperation.java index 7fb633e401..0a10664df2 100644 --- a/opendj-server-legacy/src/main/java/org/opends/server/workflowelement/localbackend/LocalBackendDeleteOperation.java +++ b/opendj-server-legacy/src/main/java/org/opends/server/workflowelement/localbackend/LocalBackendDeleteOperation.java @@ -13,7 +13,7 @@ * * Copyright 2008-2009 Sun Microsystems, Inc. * Portions Copyright 2011-2016 ForgeRock AS. - * Portions Copyright 2022-2024 3A Systems, LLC. + * Portions Copyright 2022-2025 3A Systems, LLC. */ package org.opends.server.workflowelement.localbackend; @@ -23,6 +23,7 @@ import org.forgerock.i18n.slf4j.LocalizedLogger; import org.forgerock.opendj.ldap.ResultCode; import org.forgerock.opendj.ldap.SearchScope; +import org.forgerock.opendj.ldap.controls.TransactionSpecificationRequestControl; import org.opends.server.api.AccessControlHandler; import org.opends.server.api.LocalBackend; import org.opends.server.api.ClientConnection; @@ -35,6 +36,7 @@ import org.opends.server.core.DeleteOperationWrapper; import org.opends.server.core.DirectoryServer; import org.opends.server.core.PersistentSearch; +import org.opends.server.protocols.ldap.LDAPControl; import org.opends.server.types.CanceledOperationException; import org.opends.server.types.Control; import org.forgerock.opendj.ldap.DN; @@ -43,10 +45,7 @@ import org.opends.server.types.LockManager.DNLock; import org.opends.server.types.SearchFilter; import org.opends.server.types.SynchronizationProviderResult; -import org.opends.server.types.operation.PostOperationDeleteOperation; -import org.opends.server.types.operation.PostResponseDeleteOperation; -import org.opends.server.types.operation.PostSynchronizationDeleteOperation; -import org.opends.server.types.operation.PreOperationDeleteOperation; +import org.opends.server.types.operation.*; import static org.opends.messages.CoreMessages.*; import static org.opends.server.core.DirectoryServer.*; @@ -63,7 +62,7 @@ public class LocalBackendDeleteOperation extends DeleteOperationWrapper implements PreOperationDeleteOperation, PostOperationDeleteOperation, PostResponseDeleteOperation, - PostSynchronizationDeleteOperation + PostSynchronizationDeleteOperation, RollbackOperation { private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); @@ -299,6 +298,9 @@ private void processDelete(AtomicBoolean executePostOpPlugins) return; } backend.deleteEntry(entryDN, this); + if (trx!=null) { + trx.success(this); + } } LocalBackendWorkflowElement.addPreReadResponse(this, preReadRequest, entry); @@ -416,6 +418,10 @@ else if (LocalBackendWorkflowElement.isProxyAuthzControl(oid)) { continue; } + else if (TransactionSpecificationRequestControl.OID.equals(oid)) + { + trx=getClientConnection().getTransaction(((LDAPControl)c).getValue().toString()); + } else if (c.isCritical() && !backend.supportsControl(oid)) { throw newDirectoryException(entry, ResultCode.UNAVAILABLE_CRITICAL_EXTENSION, @@ -423,6 +429,7 @@ else if (c.isCritical() && !backend.supportsControl(oid)) } } } + ClientConnection.Transaction trx=null; /** * Handle conflict resolution. @@ -488,4 +495,9 @@ private boolean processPreOperation() { } return true; } + + @Override + public void rollback() throws CanceledOperationException, DirectoryException { + backend.addEntry(entry,null); + } } diff --git a/opendj-server-legacy/src/main/java/org/opends/server/workflowelement/localbackend/LocalBackendModifyDNOperation.java b/opendj-server-legacy/src/main/java/org/opends/server/workflowelement/localbackend/LocalBackendModifyDNOperation.java index a0a575d59e..effec13f3d 100644 --- a/opendj-server-legacy/src/main/java/org/opends/server/workflowelement/localbackend/LocalBackendModifyDNOperation.java +++ b/opendj-server-legacy/src/main/java/org/opends/server/workflowelement/localbackend/LocalBackendModifyDNOperation.java @@ -13,7 +13,7 @@ * * Copyright 2008-2010 Sun Microsystems, Inc. * Portions Copyright 2011-2016 ForgeRock AS. - * Portions copyright 2024 3A Systems,LLC. + * Portions copyright 2024-2025 3A Systems,LLC. */ package org.opends.server.workflowelement.localbackend; @@ -29,6 +29,7 @@ import org.forgerock.opendj.ldap.ByteString; import org.forgerock.opendj.ldap.ModificationType; import org.forgerock.opendj.ldap.ResultCode; +import org.forgerock.opendj.ldap.controls.TransactionSpecificationRequestControl; import org.forgerock.opendj.ldap.schema.AttributeType; import org.opends.server.api.AccessControlHandler; import org.opends.server.api.LocalBackend; @@ -43,6 +44,7 @@ import org.opends.server.core.ModifyDNOperation; import org.opends.server.core.ModifyDNOperationWrapper; import org.opends.server.core.PersistentSearch; +import org.opends.server.protocols.ldap.LDAPControl; import org.opends.server.types.Attribute; import org.opends.server.types.Attributes; import org.opends.server.types.CanceledOperationException; @@ -54,10 +56,7 @@ import org.opends.server.types.Modification; import org.forgerock.opendj.ldap.RDN; import org.opends.server.types.SearchFilter; -import org.opends.server.types.operation.PostOperationModifyDNOperation; -import org.opends.server.types.operation.PostResponseModifyDNOperation; -import org.opends.server.types.operation.PostSynchronizationModifyDNOperation; -import org.opends.server.types.operation.PreOperationModifyDNOperation; +import org.opends.server.types.operation.*; import static org.opends.messages.CoreMessages.*; import static org.opends.server.core.DirectoryServer.*; @@ -75,7 +74,7 @@ public class LocalBackendModifyDNOperation implements PreOperationModifyDNOperation, PostOperationModifyDNOperation, PostResponseModifyDNOperation, - PostSynchronizationModifyDNOperation + PostSynchronizationModifyDNOperation, RollbackOperation { private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); @@ -439,6 +438,9 @@ else if (!currentBackend.equals(newBackend)) return; } currentBackend.renameEntry(entryDN, newEntry, this); + if (trx!=null) { + trx.success(this); + } } // Attach the pre-read and/or post-read controls to the response if @@ -576,6 +578,10 @@ else if (LocalBackendWorkflowElement.isProxyAuthzControl(oid)) { continue; } + else if (TransactionSpecificationRequestControl.OID.equals(oid)) + { + trx=getClientConnection().getTransaction(((LDAPControl)c).getValue().toString()); + } else if (c.isCritical() && !backend.supportsControl(oid)) { throw new DirectoryException(ResultCode.UNAVAILABLE_CRITICAL_EXTENSION, @@ -583,6 +589,7 @@ else if (c.isCritical() && !backend.supportsControl(oid)) } } } + ClientConnection.Transaction trx=null; private AccessControlHandler getAccessControlHandler() { @@ -816,4 +823,9 @@ private void processSynchPostOperationPlugins() } } } + + @Override + public void rollback() throws CanceledOperationException, DirectoryException { + backend.renameEntry(newEntry.getName(), currentEntry, this); + } } diff --git a/opendj-server-legacy/src/main/java/org/opends/server/workflowelement/localbackend/LocalBackendModifyOperation.java b/opendj-server-legacy/src/main/java/org/opends/server/workflowelement/localbackend/LocalBackendModifyOperation.java index 6995427b84..f2c7f78857 100644 --- a/opendj-server-legacy/src/main/java/org/opends/server/workflowelement/localbackend/LocalBackendModifyOperation.java +++ b/opendj-server-legacy/src/main/java/org/opends/server/workflowelement/localbackend/LocalBackendModifyOperation.java @@ -13,7 +13,7 @@ * * Copyright 2008-2011 Sun Microsystems, Inc. * Portions Copyright 2011-2016 ForgeRock AS. - * Portions copyright 2024 3A Systems,LLC. + * Portions Copyright 2024-2025 3A Systems,LLC. */ package org.opends.server.workflowelement.localbackend; @@ -35,6 +35,7 @@ import org.forgerock.opendj.ldap.RDN; import org.forgerock.opendj.ldap.ResultCode; import org.forgerock.opendj.ldap.controls.RelaxRulesControl; +import org.forgerock.opendj.ldap.controls.TransactionSpecificationRequestControl; import org.forgerock.opendj.ldap.schema.AttributeType; import org.forgerock.opendj.ldap.schema.MatchingRule; import org.forgerock.opendj.ldap.schema.ObjectClass; @@ -60,6 +61,7 @@ import org.opends.server.core.PasswordPolicy; import org.opends.server.core.PasswordPolicyState; import org.opends.server.core.PersistentSearch; +import org.opends.server.protocols.ldap.LDAPControl; import org.opends.server.schema.AuthPasswordSyntax; import org.opends.server.schema.UserPasswordSyntax; import org.opends.server.types.AcceptRejectWarn; @@ -77,10 +79,7 @@ import org.opends.server.types.Privilege; import org.opends.server.types.SearchFilter; import org.opends.server.types.SynchronizationProviderResult; -import org.opends.server.types.operation.PostOperationModifyOperation; -import org.opends.server.types.operation.PostResponseModifyOperation; -import org.opends.server.types.operation.PostSynchronizationModifyOperation; -import org.opends.server.types.operation.PreOperationModifyOperation; +import org.opends.server.types.operation.*; import static org.opends.messages.CoreMessages.*; import static org.opends.server.config.ConfigConstants.*; @@ -96,7 +95,7 @@ public class LocalBackendModifyOperation extends ModifyOperationWrapper implements PreOperationModifyOperation, PostOperationModifyOperation, PostResponseModifyOperation, - PostSynchronizationModifyOperation + PostSynchronizationModifyOperation, RollbackOperation { private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); @@ -500,6 +499,9 @@ private void processModify() throws CanceledOperationException } backend.replaceEntry(currentEntry, modifiedEntry, this); + if (trx!=null) { + trx.success(this); + } if (isAuthnManagedLocally()) { @@ -697,6 +699,10 @@ else if (RelaxRulesControl.OID.equals(oid)) { RelaxRulesControlRequested = true; } + else if (TransactionSpecificationRequestControl.OID.equals(oid)) + { + trx=getClientConnection().getTransaction(((LDAPControl)c).getValue().toString()); + } else if (c.isCritical() && !backend.supportsControl(oid)) { throw newDirectoryException(currentEntry, ResultCode.UNAVAILABLE_CRITICAL_EXTENSION, @@ -704,6 +710,7 @@ else if (c.isCritical() && !backend.supportsControl(oid)) } } } + ClientConnection.Transaction trx=null; private void processNonPasswordModifications() throws DirectoryException { @@ -1658,4 +1665,9 @@ private void processSynchPostOperationPlugins() { } } } + + @Override + public void rollback() throws CanceledOperationException, DirectoryException { + backend.replaceEntry(modifiedEntry,currentEntry, this); + } } diff --git a/opendj-server-legacy/src/test/java/org/openidentityplatform/opendj/Rfc5808TestCase.java b/opendj-server-legacy/src/test/java/org/openidentityplatform/opendj/Rfc5808TestCase.java new file mode 100644 index 0000000000..916f2539be --- /dev/null +++ b/opendj-server-legacy/src/test/java/org/openidentityplatform/opendj/Rfc5808TestCase.java @@ -0,0 +1,260 @@ +/* + * The contents of this file are subject to the terms of the Common Development and + * Distribution License (the License). You may not use this file except in compliance with the + * License. + * + * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the + * specific language governing permission and limitations under the License. + * + * When distributing Covered Software, include this CDDL Header Notice in each file and include + * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL + * Header, with the fields enclosed by brackets [] replaced by your own identifying + * information: "Portions Copyright [year] [name of copyright owner]". + * + * Copyright 2025 3A Systems, LLC. + */ +package org.openidentityplatform.opendj; + + +import com.forgerock.opendj.ldap.extensions.EndTransactionExtendedRequest; +import com.forgerock.opendj.ldap.extensions.EndTransactionExtendedResult; +import com.forgerock.opendj.ldap.extensions.StartTransactionExtendedRequest; +import org.forgerock.opendj.ldap.*; +import org.forgerock.opendj.ldap.controls.TransactionSpecificationRequestControl; +import org.forgerock.opendj.ldap.requests.*; +import com.forgerock.opendj.ldap.extensions.StartTransactionExtendedResult; +import org.forgerock.opendj.ldap.responses.Result; +import org.opends.server.DirectoryServerTestCase; +import org.opends.server.TestCaseUtils; +import org.testng.Assert; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.forgerock.opendj.ldap.requests.Requests.newAddRequest; +import static org.testng.Assert.assertThrows; + +@Test(sequential = true) +public class Rfc5808TestCase extends DirectoryServerTestCase { + Connection connection; + + @BeforeClass + public void startServer() throws Exception { + TestCaseUtils.startServer(); + TestCaseUtils.initializeTestBackend(true); + + final LDAPConnectionFactory factory =new LDAPConnectionFactory("localhost", TestCaseUtils.getServerLdapPort()); + connection = factory.getConnection(); + connection.bind("cn=Directory Manager", "password".toCharArray()); + assertThat(connection.isValid()).isTrue(); + } + + @Test + public void test() throws LdapException { + //unknown transaction in TransactionSpecificationRequestControl + assertThrows(CancelledResultException.class, new Assert.ThrowingRunnable() { + @Override + public void run() throws Throwable { + AddRequest add=Requests + .newAddRequest("ou=People,o=test") + .addAttribute("objectClass", "top", "organizationalUnit") + .addAttribute("ou", "People") + .addControl(new TransactionSpecificationRequestControl("bad")) + ; + Result result = connection.add(add); + } + }); + + //unknown transaction in EndTransactionExtendedRequest + assertThrows(CancelledResultException.class, new Assert.ThrowingRunnable() { + @Override + public void run() throws Throwable { + EndTransactionExtendedResult resEnd=connection.extendedRequest(new EndTransactionExtendedRequest().setTransactionID("unknown").setCommit(true)); + } + }); + assertThrows(CancelledResultException.class, new Assert.ThrowingRunnable() { + @Override + public void run() throws Throwable { + EndTransactionExtendedResult resEnd=connection.extendedRequest(new EndTransactionExtendedRequest().setTransactionID("unknown")); + } + }); + + //commit + StartTransactionExtendedResult resStart=connection.extendedRequest(new StartTransactionExtendedRequest()); + assertThat(resStart.isSuccess()).isTrue(); + assertThat(resStart.getOID()).isEqualTo("1.3.6.1.1.21.1"); + String transactionID=resStart.getTransactionID(); + assertThat(transactionID).isNotEmpty(); + + AddRequest add=Requests + .newAddRequest("ou=People,o=test") + .addAttribute("objectClass", "top", "organizationalUnit") + .addAttribute("ou", "People") + .addControl(new TransactionSpecificationRequestControl(transactionID)) + ; + Result result = connection.add(add); + assertThat(result.isSuccess()).isTrue(); + + add= Requests.newAddRequest("sn=bjensen,ou=People,o=test") + .addAttribute("objectClass","top","person") + .addAttribute("cn","bjensen") + .addControl(new TransactionSpecificationRequestControl(transactionID)) + ; + result = connection.add(add); + assertThat(result.isSuccess()).isTrue(); + + ModifyDNRequest mdn=Requests.newModifyDNRequest("sn=bjensen,ou=People,o=test","sn=bjensen2") + .addControl(new TransactionSpecificationRequestControl(transactionID)) + ; + result = connection.modifyDN(mdn); + assertThat(result.isSuccess()).isTrue(); + + ModifyRequest edit= Requests.newModifyRequest("sn=bjensen2,ou=People,o=test") + .addModification(ModificationType.REPLACE,"cn","bjensen2") + .addControl(new TransactionSpecificationRequestControl(transactionID)) + ; + result = connection.modify(edit); + assertThat(result.isSuccess()).isTrue(); + + DeleteRequest delete=Requests.newDeleteRequest("sn=bjensen2,ou=People,o=test") + .addControl(new TransactionSpecificationRequestControl(transactionID)) + ; + result = connection.delete(delete); + assertThat(result.isSuccess()).isTrue(); + + assertThrows(EntryNotFoundException.class, new Assert.ThrowingRunnable() { + @Override + public void run() throws Throwable { + connection.searchSingleEntry("o=test",SearchScope.SINGLE_LEVEL,"(ou=People)"); + } + }); + + EndTransactionExtendedResult resEnd=connection.extendedRequest(new EndTransactionExtendedRequest().setTransactionID(transactionID)); + assertThat(resEnd.isSuccess()).isTrue(); + assertThat(resEnd.getOID()).isEqualTo("1.3.6.1.1.21.3"); + + //check commit successfully + assertThat(connection.searchSingleEntry("o=test",SearchScope.SINGLE_LEVEL,"(ou=People)")).isNotNull(); + + //check transaction finished + String finalTransactionID = transactionID; + assertThrows(CancelledResultException.class, new Assert.ThrowingRunnable() { + @Override + public void run() throws Throwable { + EndTransactionExtendedResult resEnd=connection.extendedRequest(new EndTransactionExtendedRequest().setTransactionID(finalTransactionID).setCommit(true)); + } + }); + + //rollback by EndTransactionExtendedRequest + resStart=connection.extendedRequest(new StartTransactionExtendedRequest()); + assertThat(resStart.isSuccess()).isTrue(); + assertThat(resStart.getOID()).isEqualTo("1.3.6.1.1.21.1"); + transactionID=resStart.getTransactionID(); + assertThat(transactionID).isNotEmpty(); + + add=Requests + .newAddRequest("ou=People2,o=test") + .addAttribute("objectClass", "top", "organizationalUnit") + .addAttribute("ou", "People2") + .addControl(new TransactionSpecificationRequestControl(transactionID)) + ; + result = connection.add(add); + assertThat(result.isSuccess()).isTrue(); + + resEnd=connection.extendedRequest(new EndTransactionExtendedRequest().setTransactionID(transactionID).setCommit(false)); + assertThat(resEnd.isSuccess()).isTrue(); + assertThat(resEnd.getOID()).isEqualTo("1.3.6.1.1.21.3"); + + //check transaction finished + String finalTransactionID1 = transactionID; + assertThrows(CancelledResultException.class, new Assert.ThrowingRunnable() { + @Override + public void run() throws Throwable { + EndTransactionExtendedResult resEnd=connection.extendedRequest(new EndTransactionExtendedRequest().setTransactionID(finalTransactionID1).setCommit(false)); + } + }); + + //check rollback successfully + assertThrows(EntryNotFoundException.class, new Assert.ThrowingRunnable() { + @Override + public void run() throws Throwable { + connection.searchSingleEntry("o=test",SearchScope.SINGLE_LEVEL,"(ou=People2)"); + } + }); + + //rollback by error + resStart=connection.extendedRequest(new StartTransactionExtendedRequest()); + assertThat(resStart.isSuccess()).isTrue(); + assertThat(resStart.getOID()).isEqualTo("1.3.6.1.1.21.1"); + transactionID=resStart.getTransactionID(); + assertThat(transactionID).isNotEmpty(); + + add= Requests.newAddRequest("sn=bjensen0,ou=People,o=test") + .addAttribute("objectClass","top","person") + .addAttribute("cn","bjensen0") + .addControl(new TransactionSpecificationRequestControl(transactionID)) + ; + result = connection.add(add); + assertThat(result.isSuccess()).isTrue(); + + add= Requests.newAddRequest("sn=bjensen,ou=People,o=test") + .addAttribute("objectClass","top","person") + .addAttribute("cn","bjensen") + .addControl(new TransactionSpecificationRequestControl(transactionID)) + ; + result = connection.add(add); + assertThat(result.isSuccess()).isTrue(); + + mdn=Requests.newModifyDNRequest("sn=bjensen,ou=People,o=test","sn=bjensen2") + .addControl(new TransactionSpecificationRequestControl(transactionID)) + ; + result = connection.modifyDN(mdn); + assertThat(result.isSuccess()).isTrue(); + + edit= Requests.newModifyRequest("sn=bjensen2,ou=People,o=test") + .addModification(ModificationType.REPLACE,"cn","bjensen2") + .addControl(new TransactionSpecificationRequestControl(transactionID)) + ; + result = connection.modify(edit); + assertThat(result.isSuccess()).isTrue(); + + delete=Requests.newDeleteRequest("sn=bjensen2,ou=People,o=test") + .addControl(new TransactionSpecificationRequestControl(transactionID)) + ; + result = connection.delete(delete); + assertThat(result.isSuccess()).isTrue(); + + delete=Requests.newDeleteRequest("sn=bjensen3,ou=People,o=test") + .addControl(new TransactionSpecificationRequestControl(transactionID)) + ; + result = connection.delete(delete); + assertThat(result.isSuccess()).isTrue(); + + String finalTransactionID3 = transactionID; + assertThrows(EntryNotFoundException.class, new Assert.ThrowingRunnable() { + @Override + public void run() throws Throwable { + EndTransactionExtendedResult resEnd=connection.extendedRequest(new EndTransactionExtendedRequest().setTransactionID(finalTransactionID3).setCommit(true)); + } + }); + + //check transaction finished + String finalTransactionID2 = transactionID; + assertThrows(CancelledResultException.class, new Assert.ThrowingRunnable() { + @Override + public void run() throws Throwable { + EndTransactionExtendedResult resEnd=connection.extendedRequest(new EndTransactionExtendedRequest().setTransactionID(finalTransactionID2).setCommit(false)); + } + }); + + //check rollback successfully + assertThrows(EntryNotFoundException.class, new Assert.ThrowingRunnable() { + @Override + public void run() throws Throwable { + connection.searchSingleEntry("ou=People,o=test",SearchScope.SINGLE_LEVEL,"(cn=bjensen0)"); + } + }); + + } +}