From b9489f170636cee8af4c5edb824a34e21cc63195 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Fri, 30 May 2025 09:18:41 +0000
Subject: [PATCH] [#462] RFC5805 Lightweight Directory Access Protocol (LDAP) Transactions (#469)
---
opendj-server-legacy/src/main/java/org/opends/server/api/ClientConnection.java | 52 +
opendj-server-legacy/src/main/java/org/opends/server/workflowelement/localbackend/LocalBackendDeleteOperation.java | 24
opendj-server-legacy/src/main/java/org/opends/server/extensions/StartTransactionExtendedOperation.java | 66 ++
opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/StartTransactionExtendedRequest.java | 101 +++
opendj-server-legacy/src/main/java/org/opends/server/workflowelement/localbackend/LocalBackendModifyOperation.java | 24
opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/TransactionSpecificationRequestControl.java | 82 +++
opendj-server-legacy/src/test/java/org/openidentityplatform/opendj/Rfc5808TestCase.java | 260 +++++++++
opendj-server-legacy/resource/schema/02-config.ldif | 11
opendj-server-legacy/src/main/java/org/opends/server/workflowelement/localbackend/LocalBackendModifyDNOperation.java | 24
opendj-server-legacy/src/main/java/org/opends/server/workflowelement/localbackend/LocalBackendAddOperation.java | 25
opendj-server-legacy/src/main/java/org/opends/server/extensions/TraditionalWorkerThread.java | 29 +
opendj-server-legacy/src/main/java/org/opends/server/types/AbstractOperation.java | 14
opendj-server-legacy/resource/config/config.ldif | 17
opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/EndTransactionExtendedResult.java | 168 ++++++
opendj-doc-generated-ref/src/main/asciidoc/reference/appendix-standards.adoc | 9
opendj-server-legacy/src/main/java/org/opends/server/types/operation/RollbackOperation.java | 26
opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/StartTransactionExtendedOperationHandlerConfiguration.xml | 55 ++
opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/AbortedTransactionExtendedResult.java | 94 +++
opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/StartTransactionExtendedResult.java | 96 +++
opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/EndTransactionExtendedOperationHandlerConfiguration.xml | 75 ++
opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/EndTransactionExtendedRequest.java | 182 ++++++
opendj-server-legacy/src/main/java/org/opends/server/extensions/EndTransactionExtendedOperation.java | 131 ++++
22 files changed, 1,531 insertions(+), 34 deletions(-)
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 0000000..35c4981
--- /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<AbortedTransactionExtendedResult> {
+ @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 0000000..5b898a1
--- /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<EndTransactionExtendedRequest, EndTransactionExtendedResult> {
+
+ 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<EndTransactionExtendedResult> 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<EndTransactionExtendedResult> {
+ @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<Control> 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 0000000..a4ba23e
--- /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<EndTransactionExtendedResult> {
+ @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<Integer, List<Control>> opResponseControls= new TreeMap<>();
+
+ public EndTransactionExtendedResult success(Integer messageID, List<Control> 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<Integer, List<Control>> 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 0000000..647063d
--- /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<StartTransactionExtendedRequest, StartTransactionExtendedResult> {
+ 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<StartTransactionExtendedResult> 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<StartTransactionExtendedResult> {
+ @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 0000000..40dbf04
--- /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<StartTransactionExtendedResult> {
+ @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 0000000..2d4f589
--- /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<TransactionSpecificationRequestControl> DECODER = new ControlDecoder<TransactionSpecificationRequestControl>() {
+ @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 5d3707f..926f3b9 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 @@
+
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 0000000..3631751
--- /dev/null
+++ b/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/EndTransactionExtendedOperationHandlerConfiguration.xml
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+ ! -->
+<adm:managed-object name="end-transaction-extended-operation-handler"
+ plural-name="end-transaction-extended-operation-handlers"
+ package="org.forgerock.opendj.server.config"
+ extends="extended-operation-handler"
+ xmlns:adm="http://opendj.forgerock.org/admin"
+ xmlns:ldap="http://opendj.forgerock.org/admin-ldap">
+ <adm:synopsis>
+ The
+ <adm:user-friendly-name />
+ 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).
+ </adm:synopsis>
+ <adm:profile name="ldap">
+ <ldap:object-class>
+ <ldap:name>
+ ds-cfg-end-transaction-extended-operation-handler
+ </ldap:name>
+ <ldap:superior>ds-cfg-extended-operation-handler</ldap:superior>
+ </ldap:object-class>
+ </adm:profile>
+ <adm:property-override name="java-class" advanced="true">
+ <adm:default-behavior>
+ <adm:defined>
+ <adm:value>
+ org.opends.server.extensions.EndTransactionExtendedOperation
+ </adm:value>
+ </adm:defined>
+ </adm:default-behavior>
+ </adm:property-override>
+</adm:managed-object>
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 0000000..e70290a
--- /dev/null
+++ b/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/StartTransactionExtendedOperationHandlerConfiguration.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+ ! -->
+<adm:managed-object name="start-transaction-extended-operation-handler"
+ plural-name="start-transaction-extended-operation-handlers"
+ package="org.forgerock.opendj.server.config"
+ extends="extended-operation-handler"
+ xmlns:adm="http://opendj.forgerock.org/admin"
+ xmlns:ldap="http://opendj.forgerock.org/admin-ldap">
+ <adm:synopsis>
+ The
+ <adm:user-friendly-name />
+ 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.
+ </adm:synopsis>
+ <adm:profile name="ldap">
+ <ldap:object-class>
+ <ldap:name>
+ ds-cfg-start-transaction-extended-operation-handler
+ </ldap:name>
+ <ldap:superior>ds-cfg-extended-operation-handler</ldap:superior>
+ </ldap:object-class>
+ </adm:profile>
+ <adm:property-override name="java-class" advanced="true">
+ <adm:default-behavior>
+ <adm:defined>
+ <adm:value>
+ org.opends.server.extensions.StartTransactionExtendedOperation
+ </adm:value>
+ </adm:defined>
+ </adm:default-behavior>
+ </adm:property-override>
+</adm:managed-object>
diff --git a/opendj-server-legacy/resource/config/config.ldif b/opendj-server-legacy/resource/config/config.ldif
index f836b18..0683e93 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 @@
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 a950d83..2f8cc52 100644
--- a/opendj-server-legacy/resource/schema/02-config.ldif
+++ b/opendj-server-legacy/resource/schema/02-config.ldif
@@ -6265,4 +6265,13 @@
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 7487b3d..a891fee 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 @@
{
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<Operation> waiting=new LinkedList<>();
+ public void add(Operation operation) {
+ waiting.add(operation);
+ }
+
+ public Queue<Operation> getWaiting() {
+ return waiting;
+ }
+
+ public void clear() {
+ transactions.remove(getTransactionId());
+ }
+ final Deque<RollbackOperation> completed =new ArrayDeque<>();
+ public void success(RollbackOperation operation) {
+ completed.add(operation);
+ }
+
+ public Deque<RollbackOperation> getCompleted() {
+ return completed;
+ }
+ }
+
+ Map<String,Transaction> 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 0000000..9ebb554
--- /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<EndTransactionExtendedOperationHandlerCfg>
+{
+ 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 0000000..b7fb0a3
--- /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<StartTransactionExtendedOperationHandlerCfg>
+{
+ 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 d88bdca..1ceae70 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 @@
{
// 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 61ee9f2..5f5c67c 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 @@
}
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 0000000..cdeb151
--- /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 1b86927..03a1e3b 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 @@
}
backend.addEntry(entry, this);
+ if (trx!=null) {
+ trx.success(this);
+ }
}
LocalBackendWorkflowElement.addPostReadResponse(this, postReadRequest,
@@ -496,7 +498,10 @@
}
}
-
+ @Override
+ public void rollback() throws CanceledOperationException, DirectoryException {
+ backend.deleteEntry(entryDN,null);
+ }
private void processSynchPostOperationPlugins()
{
@@ -968,6 +973,10 @@
{
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 @@
}
}
}
+ 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 7fb633e..0a10664 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 @@
extends DeleteOperationWrapper
implements PreOperationDeleteOperation, PostOperationDeleteOperation,
PostResponseDeleteOperation,
- PostSynchronizationDeleteOperation
+ PostSynchronizationDeleteOperation, RollbackOperation
{
private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
@@ -299,6 +298,9 @@
return;
}
backend.deleteEntry(entryDN, this);
+ if (trx!=null) {
+ trx.success(this);
+ }
}
LocalBackendWorkflowElement.addPreReadResponse(this, preReadRequest, entry);
@@ -416,6 +418,10 @@
{
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 @@
}
}
}
+ ClientConnection.Transaction trx=null;
/**
* Handle conflict resolution.
@@ -488,4 +495,9 @@
}
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 a0a575d..effec13 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 @@
implements PreOperationModifyDNOperation,
PostOperationModifyDNOperation,
PostResponseModifyDNOperation,
- PostSynchronizationModifyDNOperation
+ PostSynchronizationModifyDNOperation, RollbackOperation
{
private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
@@ -439,6 +438,9 @@
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 @@
{
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 @@
}
}
}
+ ClientConnection.Transaction trx=null;
private AccessControlHandler<?> getAccessControlHandler()
{
@@ -816,4 +823,9 @@
}
}
}
+
+ @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 6995427..f2c7f78 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 @@
extends ModifyOperationWrapper
implements PreOperationModifyOperation, PostOperationModifyOperation,
PostResponseModifyOperation,
- PostSynchronizationModifyOperation
+ PostSynchronizationModifyOperation, RollbackOperation
{
private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
@@ -500,6 +499,9 @@
}
backend.replaceEntry(currentEntry, modifiedEntry, this);
+ if (trx!=null) {
+ trx.success(this);
+ }
if (isAuthnManagedLocally())
{
@@ -697,6 +699,10 @@
{
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 @@
}
}
}
+ ClientConnection.Transaction trx=null;
private void processNonPasswordModifications() throws DirectoryException
{
@@ -1658,4 +1665,9 @@
}
}
}
+
+ @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 0000000..916f253
--- /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)");
+ }
+ });
+
+ }
+}
--
Gitblit v1.10.0