Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#462] RFC5805 Lightweight Directory Access Protocol (LDAP) Transactions #469

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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() +
")";
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
}

Loading