Skip to content
Merged
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
Expand Up @@ -12,7 +12,7 @@
information: "Portions copyright [year] [name of copyright owner]".

Copyright 2017 ForgeRock AS.
Portions Copyright 2024 3A Systems LLC.
Portions Copyright 2024-2026 3A Systems LLC.
////

:figure-caption!:
Expand Down Expand Up @@ -264,7 +264,10 @@ The LDAP Relax Rules Control::
Object Identifier: 1.3.6.1.4.1.4203.666.5.12

+
Internet-Draft: link:https://tools.ietf.org/html/draft-zeilenga-ldap-relax-03[ddraft-zeilenga-ldap-relax-03 - The LDAP Relax Rules Control, window=\_top]
Internet-Draft: link:https://tools.ietf.org/html/draft-zeilenga-ldap-relax-03[draft-zeilenga-ldap-relax-03 - The LDAP Relax Rules Control, window=\_top]

+
Lets a client with the `bypass-acl` privilege add or modify attributes marked `NO-USER-MODIFICATION` or `OBSOLETE`, and skips the schema check of the resulting entry. The values the client supplies for `creatorsName`, `createTimestamp`, `modifiersName`, `modifyTimestamp` and `pwdChangedTime` are kept, and a pre-encoded password is accepted. The change is an ordinary one otherwise: it is logged, and it is replicated like any other.

--

Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
! CCPL HEADER END
!
! Copyright 2011 ForgeRock AS
! Portions copyright 2024 3A Systems,LLC.
! Portions copyright 2024-2026 3A Systems,LLC.
!
-->
<appendix xml:id='appendix-controls'
Expand Down Expand Up @@ -457,7 +457,16 @@
<para>Object Identifier: 1.3.6.1.4.1.4203.666.5.12</para>
<para>Internet-Draft: <link
xlink:href='https://tools.ietf.org/html/draft-zeilenga-ldap-relax-03'
>ddraft-zeilenga-ldap-relax-03 - The LDAP Relax Rules Control</link></para>
>draft-zeilenga-ldap-relax-03 - The LDAP Relax Rules Control</link></para>
<para>Lets a client with the <literal>bypass-acl</literal> privilege add
or modify attributes marked <literal>NO-USER-MODIFICATION</literal> or
<literal>OBSOLETE</literal>, and skips the schema check of the resulting
entry. The values the client supplies for <literal>creatorsName</literal>,
<literal>createTimestamp</literal>, <literal>modifiersName</literal>,
<literal>modifyTimestamp</literal> and <literal>pwdChangedTime</literal>
are kept, and a pre-encoded password is accepted. The change is an
ordinary one otherwise: it is logged, and it is replicated like any
other.</para>
</listitem>
</varlistentry>
</variablelist>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*
* Copyright 2007-2010 Sun Microsystems, Inc.
* Portions Copyright 2013-2016 ForgeRock AS.
* Portions Copyright 2026 3A Systems, LLC.
*/
package org.opends.server.core;

Expand Down Expand Up @@ -288,11 +289,13 @@ private final void computeObjectClassesAndAttributes()
AttributeType attrType = attrDesc.getAttributeType();

// If the attribute type is marked "NO-USER-MODIFICATION" then fail
// unless this is an internal operation or is related to
// synchronization in some way.
// unless this is an internal operation, is related to
// synchronization in some way, or the client asked for the rules
// to be relaxed.
if (attrType.isNoUserModification()
&& !isInternalOperation()
&& !isSynchronizationOperation())
&& !isSynchronizationOperation()
&& !isRelaxRulesRequested(this))
{
throw new LDAPException(LDAPResultCode.UNWILLING_TO_PERFORM,
ERR_ADD_ATTR_IS_NO_USER_MOD.get(entryDN, attrDesc));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*
* Copyright 2006-2008 Sun Microsystems, Inc.
* Portions Copyright 2014-2016 ForgeRock AS.
* Portions Copyright 2026 3A Systems, LLC.
*/
package org.opends.server.plugins;

Expand All @@ -32,6 +33,7 @@
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.ModificationType;
import org.forgerock.opendj.ldap.schema.AttributeType;
import org.forgerock.opendj.server.config.meta.PluginCfgDefn;
import org.forgerock.opendj.server.config.server.LastModPluginCfg;
import org.forgerock.opendj.server.config.server.PluginCfg;
Expand All @@ -47,6 +49,7 @@
import org.opends.server.types.operation.PreOperationAddOperation;
import org.opends.server.types.operation.PreOperationModifyDNOperation;
import org.opends.server.types.operation.PreOperationModifyOperation;
import org.opends.server.workflowelement.localbackend.LocalBackendWorkflowElement;

/**
* This class implements a Directory Server plugin that will add the
Expand Down Expand Up @@ -117,25 +120,31 @@ public final void finalizePlugin()
doPreOperation(PreOperationAddOperation addOperation)
{
// Create the attribute list for the creatorsName attribute, if appropriate.
AttributeBuilder builder = new AttributeBuilder(getCreatorsNameAttributeType());
DN creatorDN = addOperation.getAuthorizationDN();
if (creatorDN == null)
if (!isSuppliedUnderRelaxRules(addOperation, getCreatorsNameAttributeType()))
{
// This must mean that the operation was performed anonymously.
// Even so, we still need to update the creatorsName attribute.
builder.add(ByteString.empty());
}
else
{
builder.add(creatorDN.toString());
AttributeBuilder builder = new AttributeBuilder(getCreatorsNameAttributeType());
DN creatorDN = addOperation.getAuthorizationDN();
if (creatorDN == null)
{
// This must mean that the operation was performed anonymously.
// Even so, we still need to update the creatorsName attribute.
builder.add(ByteString.empty());
}
else
{
builder.add(creatorDN.toString());
}
addOperation.setAttribute(getCreatorsNameAttributeType(), builder.toAttributeList());
}
addOperation.setAttribute(getCreatorsNameAttributeType(), builder.toAttributeList());


// Create the attribute list for the createTimestamp attribute.
List<Attribute> timeList = Attributes.createAsList(
getCreateTimestampAttributeType(), OP_ATTR_CREATE_TIMESTAMP, getGMTTime());
addOperation.setAttribute(getCreateTimestampAttributeType(), timeList);
if (!isSuppliedUnderRelaxRules(addOperation, getCreateTimestampAttributeType()))
{
List<Attribute> timeList = Attributes.createAsList(
getCreateTimestampAttributeType(), OP_ATTR_CREATE_TIMESTAMP, getGMTTime());
addOperation.setAttribute(getCreateTimestampAttributeType(), timeList);
}

// We shouldn't ever need to return a non-success result.
return PluginResult.PreOperation.continueOperationProcessing();
Expand All @@ -148,56 +157,92 @@ public final void finalizePlugin()
doPreOperation(PreOperationModifyOperation modifyOperation)
{
// Create the modifiersName attribute.
AttributeBuilder builder = new AttributeBuilder(getModifiersNameAttributeType());
DN modifierDN = modifyOperation.getAuthorizationDN();
if (modifierDN == null)
if (!isSuppliedUnderRelaxRules(modifyOperation, getModifiersNameAttributeType()))
{
// This must mean that the operation was performed anonymously.
// Even so, we still need to update the modifiersName attribute.
builder.add(ByteString.empty());
}
else
{
builder.add(modifierDN.toString());
}
Attribute nameAttr = builder.toAttribute();
try
{
modifyOperation.addModification(new Modification(ModificationType.REPLACE,
nameAttr, true));
}
catch (DirectoryException de)
{
logger.traceException(de);
AttributeBuilder builder = new AttributeBuilder(getModifiersNameAttributeType());
DN modifierDN = modifyOperation.getAuthorizationDN();
if (modifierDN == null)
{
// This must mean that the operation was performed anonymously.
// Even so, we still need to update the modifiersName attribute.
builder.add(ByteString.empty());
}
else
{
builder.add(modifierDN.toString());
}
Attribute nameAttr = builder.toAttribute();
try
{
modifyOperation.addModification(new Modification(ModificationType.REPLACE,
nameAttr, true));
}
catch (DirectoryException de)
{
logger.traceException(de);

// This should never happen.
return PluginResult.PreOperation.stopProcessing(
DirectoryConfig.getServerErrorResultCode(), de.getMessageObject());
// This should never happen.
return PluginResult.PreOperation.stopProcessing(
DirectoryConfig.getServerErrorResultCode(), de.getMessageObject());
}
}


// Create the modifyTimestamp attribute.
Attribute timeAttr = Attributes.create(getModifyTimestampAttributeType(),
OP_ATTR_MODIFY_TIMESTAMP, getGMTTime());
try
{
modifyOperation.addModification(new Modification(ModificationType.REPLACE,
timeAttr, true));
}
catch (DirectoryException de)
if (!isSuppliedUnderRelaxRules(modifyOperation, getModifyTimestampAttributeType()))
{
logger.traceException(de);
Attribute timeAttr = Attributes.create(getModifyTimestampAttributeType(),
OP_ATTR_MODIFY_TIMESTAMP, getGMTTime());
try
{
modifyOperation.addModification(new Modification(ModificationType.REPLACE,
timeAttr, true));
}
catch (DirectoryException de)
{
logger.traceException(de);

// This should never happen.
return PluginResult.PreOperation.stopProcessing(
DirectoryConfig.getServerErrorResultCode(), de.getMessageObject());
// This should never happen.
return PluginResult.PreOperation.stopProcessing(
DirectoryConfig.getServerErrorResultCode(), de.getMessageObject());
}
}


// We shouldn't ever need to return a non-success result.
return PluginResult.PreOperation.continueOperationProcessing();
}

/**
* Indicates whether a client relaxing the rules supplies the provided attribute in the entry it
* adds: such a value is kept, the way it is kept when the change is replayed on another replica.
*/
private static boolean isSuppliedUnderRelaxRules(PreOperationAddOperation addOperation, AttributeType type)
{
return LocalBackendWorkflowElement.isRelaxRulesRequested(addOperation)
&& addOperation.getOperationalAttributes().containsKey(type);
}

/**
* Indicates whether a client relaxing the rules modifies the provided attribute: such a value is
* kept, the way it is kept when the change is replayed on another replica.
*/
private static boolean isSuppliedUnderRelaxRules(PreOperationModifyOperation modifyOperation, AttributeType type)
{
if (!LocalBackendWorkflowElement.isRelaxRulesRequested(modifyOperation))
{
return false;
}
for (Modification m : modifyOperation.getModifications())
{
if (m.getAttribute().getAttributeDescription().getAttributeType().equals(type))
{
return true;
}
}
return false;
}



@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,19 @@ public class LocalBackendAddOperation
private Map<AttributeType, List<Attribute>> operationalAttributes;
/** The set of user attributes for the entry to add. */
private Map<AttributeType, List<Attribute>> userAttributes;
/** Indicates whether the request included the RelaxRules request control. */
private boolean RelaxRulesControlRequested=false;
/**
* Indicates whether the rules are relaxed on this change: the request included the Relax Rules
* request control, and the client has the {@code bypass-acl} privilege.
* <p>
* The checks of the attributes and of the schema run before the request controls are
* processed, so this is decided up front, from the request as it came and from the
* authentication identity of the client. Once the controls are processed,
* {@link #relaxedRulesStillHold()} verifies it against the identity the request runs as.
*
* @see LocalBackendWorkflowElement#isRelaxRulesRequested(org.opends.server.types.operation.PluginOperation)
*/
private final boolean relaxRules;

/**
* Creates a new operation that may be used to add a new entry in a
* local backend of the Directory Server.
Expand All @@ -123,11 +134,28 @@ public LocalBackendAddOperation(AddOperation add)
super(add);

LocalBackendWorkflowElement.attachLocalOperation (add, this);
relaxRules = LocalBackendWorkflowElement.isRelaxRulesRequested(this)
&& getClientConnection().hasPrivilege(Privilege.BYPASS_ACL, this);
}

@Override
public boolean isSynchronizationOperation() {
return super.isSynchronizationOperation()||RelaxRulesControlRequested;
/**
* Indicates whether the request may go on as far as the Relax Rules control is concerned, once
* the request controls the client may not use are removed and the proxied authorization, if
* any, is applied.
* <p>
* A control still there needs the {@code bypass-acl} privilege of the identity the request
* runs as, which may not be the one {@link #relaxRules} was decided for. A control which is
* gone after the rules were relaxed on the checks already run cannot leave them relaxed. A
* control which is gone and relaxed nothing leaves an ordinary request.
*/
private boolean relaxedRulesStillHold()
{
final boolean controlKept = LocalBackendWorkflowElement.isRelaxRulesRequested(this);
if (controlKept)
{
return getClientConnection().hasPrivilege(Privilege.BYPASS_ACL, this);
}
return !relaxRules;
}


Expand Down Expand Up @@ -382,10 +410,11 @@ private void processAdd(ClientConnection clientConnection,
}

// If the server is configured to check schema and the
// operation is not a synchronization operation,
// check to see if the entry is valid according to the server schema,
// and also whether its attributes are valid according to their syntax.
if (DirectoryServer.getCoreConfigManager().isCheckSchema() && !isSynchronizationOperation())
// operation is not a synchronization operation nor one whose rules the
// client asked to relax, check to see if the entry is valid according to
// the server schema, and also whether its attributes are valid according
// to their syntax.
if (DirectoryServer.getCoreConfigManager().isCheckSchema() && !isSynchronizationOperation() && !relaxRules)
{
checkSchema(parentEntry);
}
Expand Down Expand Up @@ -413,7 +442,7 @@ private void processAdd(ClientConnection clientConnection,
// sensitive information to the client.
try
{
if (!getAccessControlHandler().isAllowed(this) || (RelaxRulesControlRequested && !clientConnection.hasPrivilege(Privilege.BYPASS_ACL, this)))
if (!getAccessControlHandler().isAllowed(this) || !relaxedRulesStillHold())
{
setResultCodeAndMessageNoInfoDisclosure(entryDN,
ResultCode.INSUFFICIENT_ACCESS_RIGHTS,
Expand Down Expand Up @@ -530,7 +559,8 @@ private boolean checkHasReadOnlyAttributes(
{
if (at.isNoUserModification()
&& !isInternalOperation()
&& !isSynchronizationOperation())
&& !isSynchronizationOperation()
&& !relaxRules)
{
setResultCodeAndMessageNoInfoDisclosure(entryDN,
ResultCode.CONSTRAINT_VIOLATION,
Expand Down Expand Up @@ -698,7 +728,8 @@ else if (attrList.size() > 1)
: UserPasswordSyntax.isEncoded(value);
if (isPreEncoded)
{
if (isInternalOperation() || passwordPolicy.isAllowPreEncodedPasswords())
// A client relaxing the rules may bring a password encoded elsewhere, e.g. to migrate it.
if (isInternalOperation() || passwordPolicy.isAllowPreEncodedPasswords() || relaxRules)
{
builder.add(value);
continue;
Expand Down Expand Up @@ -761,10 +792,15 @@ else if (attrList.size() > 1)
entry.replaceAttribute(builder.toAttribute());


// Set the password changed time attribute.
// Set the password changed time attribute, unless a client relaxing the
// rules supplies the time the password was changed at.
Attribute changedTime = Attributes.create(
OP_ATTR_PWPOLICY_CHANGED_TIME, TimeThread.getGeneralizedTime());
entry.putAttribute(changedTime.getAttributeDescription().getAttributeType(), newArrayList(changedTime));
AttributeType changedTimeType = changedTime.getAttributeDescription().getAttributeType();
if (!relaxRules || !entry.hasAttribute(changedTimeType))
{
entry.putAttribute(changedTimeType, newArrayList(changedTime));
}


// If we should force change on add, then set the appropriate flag.
Expand Down Expand Up @@ -976,7 +1012,7 @@ else if (OID_PASSWORD_POLICY_CONTROL.equals(oid))
}
else if (RelaxRulesControl.OID.equals(oid))
{
RelaxRulesControlRequested = true;
// Already taken into account: see relaxRules.
}
else if (TransactionSpecificationRequestControl.OID.equals(oid))
{
Expand Down
Loading
Loading