Sync conflicts in ForgeRock Directory Services (DS) can be a nightmare, especially when they occur frequently. I’ve debugged this 100+ times, and each time it feels like starting over. But once you understand the mechanics and have a solid automation strategy, it saves you hours of manual intervention.

The Problem

When ForgeRock DS synchronizes data between different sources, conflicts can arise if the same attribute is modified simultaneously by different processes. This results in ds-sync-conflict errors, which need to be resolved manually unless you handle them programmatically. These conflicts can disrupt user experiences and lead to inconsistent data states across your systems.

Identifying ds-sync-conflict Types

Before automating conflict resolution, you need to know what kinds of conflicts you’re dealing with. Common types include:

  • Attribute Value Conflicts: Different values for the same attribute from different sources.
  • Object Conflicts: Entire objects being created or modified in conflicting ways.
  • Delete Conflicts: Objects being deleted in one source while being modified in another.

To identify these conflicts, check the DS logs for ds-sync-conflict messages. They typically look something like this:

[23/Jan/2025:10:00:00 +0000] category=SYNC severity=ERROR msgID=56789 msg=ds-sync-conflict: Conflict detected for entry 'uid=jdoe,ou=people,dc=example,dc=com': Attribute 'mail' has conflicting values '[email protected]' and '[email protected]'

Understanding Sync Configurations

Before diving into automation, ensure you understand your sync configurations. Key components include:

  • Source and Target Mappings: Define how data flows between sources and targets.
  • Conflict Handling Policies: Specify how conflicts should be handled.
  • Reconciliation Rules: Determine how data consistency is maintained.

Here’s an example snippet from a DS configuration file:

<recon>
    <source-object-class>person</source-object-class>
    <target-object-class>inetOrgPerson</target-object-class>
    <conflict-resolution-policy>
        <policy-name>manual</policy-name>
    </conflict-resolution-policy>
</recon>

In this example, conflicts are set to be resolved manually. We’ll change this to automated later.

Automating Conflict Resolution

Step 1: Change Conflict Handling Policy

First, update your conflict handling policy to use a script-based approach instead of manual resolution. Modify your recon configuration:

<recon>
    <source-object-class>person</source-object-class>
    <target-object-class>inetOrgPerson</target-object-class>
    <conflict-resolution-policy>
        <policy-name>scripted</policy-name>
        <script-file>resolveConflicts.js</script-file>
    </conflict-resolution-policy>
</recon>

Step 2: Write the Script

Create a script named resolveConflicts.js that will handle different conflict types. Below is an example script that resolves attribute value conflicts by choosing the most recent value based on a timestamp attribute:

// resolveConflicts.js

/**
 * Function to resolve attribute value conflicts
 * @param {Object} sourceEntry - Entry from the source
 * @param {Object} targetEntry - Entry from the target
 * @returns {Object} - Resolved entry
 */
function resolveAttributeValueConflicts(sourceEntry, targetEntry) {
    const attributes = ['mail', 'telephoneNumber']; // List attributes to check for conflicts
    const resolvedEntry = {};

    attributes.forEach(attr => {
        if (sourceEntry[attr] && targetEntry[attr]) {
            // Compare timestamps to choose the most recent value
            const sourceTimestamp = new Date(sourceEntry['modifyTimestamp']);
            const targetTimestamp = new Date(targetEntry['modifyTimestamp']);

            if (sourceTimestamp > targetTimestamp) {
                resolvedEntry[attr] = sourceEntry[attr];
            } else {
                resolvedEntry[attr] = targetEntry[attr];
            }
        } else if (sourceEntry[attr]) {
            resolvedEntry[attr] = sourceEntry[attr];
        } else if (targetEntry[attr]) {
            resolvedEntry[attr] = targetEntry[attr];
        }
    });

    return resolvedEntry;
}

/**
 * Main function called by DS during conflict resolution
 * @param {Object} context - Context object containing source and target entries
 */
function resolveConflict(context) {
    const sourceEntry = context.sourceEntry;
    const targetEntry = context.targetEntry;

    const resolvedEntry = resolveAttributeValueConflicts(sourceEntry, targetEntry);

    // Update the target entry with resolved values
    context.targetEntry = resolvedEntry;
}

Step 3: Deploy the Script

Upload the resolveConflicts.js script to your DS server. Ensure it’s placed in a directory accessible by DS, such as /opt/opendj/scripts.

Step 4: Test the Configuration

Run a reconciliation process to test your new configuration. Monitor the logs to ensure conflicts are being resolved automatically.

dsconfig create-reconciliation-job \
    --job-name "Test Conflict Resolution" \
    --set source:sourceName \
    --set target:targetName \
    --set schedule:"0 0 * * *" \
    --set enabled:true \
    --set conflict-resolution-policy:scripted \
    --set script-file:resolveConflicts.js

Common Pitfalls

Incorrect Script Paths

Ensure the script path in your configuration matches the actual location of your script. A common mistake is using a relative path when an absolute path is required.

Incompatible Data Types

Make sure the data types in your script match those expected by DS. For example, timestamps should be parsed correctly to avoid comparison errors.

Security Considerations

  • Script Permissions: Ensure scripts are executable by the DS process user.
  • Script Integrity: Regularly review scripts for security vulnerabilities.
  • Error Handling: Implement robust error handling to prevent partial updates or data corruption.

Advanced Techniques

Logging and Monitoring

Enhance your script with logging to capture detailed information about conflict resolutions. This can help with troubleshooting and auditing.

importClass(java.util.logging.Logger);
const logger = Logger.getLogger('resolveConflicts');

function resolveConflict(context) {
    const sourceEntry = context.sourceEntry;
    const targetEntry = context.targetEntry;

    logger.info(`Resolving conflict for entry: ${sourceEntry.dn}`);
    const resolvedEntry = resolveAttributeValueConflicts(sourceEntry, targetEntry);

    // Log resolved values
    logger.info(`Resolved entry: ${JSON.stringify(resolvedEntry)}`);

    context.targetEntry = resolvedEntry;
}

Custom Conflict Rules

For more complex scenarios, implement custom rules within your script. For example, prioritize certain attributes based on business logic.

function resolveAttributeValueConflicts(sourceEntry, targetEntry) {
    const attributes = ['mail', 'telephoneNumber'];
    const resolvedEntry = {};

    attributes.forEach(attr => {
        if (attr === 'mail') {
            // Always prefer the source email
            resolvedEntry[attr] = sourceEntry[attr] || targetEntry[attr];
        } else if (attr === 'telephoneNumber') {
            // Compare timestamps for phone numbers
            const sourceTimestamp = new Date(sourceEntry['modifyTimestamp']);
            const targetTimestamp = new Date(targetEntry['modifyTimestamp']);

            if (sourceTimestamp > targetTimestamp) {
                resolvedEntry[attr] = sourceEntry[attr];
            } else {
                resolvedEntry[attr] = targetEntry[attr];
            }
        }
    });

    return resolvedEntry;
}

Integration with External Systems

Integrate your conflict resolution script with external systems for additional processing. For example, log conflicts to a ticketing system for further analysis.

importClass(java.net.HttpURLConnection);
importClass(java.net.URL);
importClass(java.io.OutputStream);

function logConflictToTicketingSystem(conflictDetails) {
    const url = new URL("https://api.example.com/tickets");
    const connection = url.openConnection();
    connection.setRequestMethod("POST");
    connection.setRequestProperty("Content-Type", "application/json");
    connection.setDoOutput(true);

    try (OutputStream os = connection.getOutputStream()) {
        byte[] input = conflictDetails.getBytes("utf-8");
        os.write(input, 0, input.length);           
    }

    const responseCode = connection.getResponseCode();
    logger.info(`Ticketing system response code: ${responseCode}`);
}

function resolveConflict(context) {
    const sourceEntry = context.sourceEntry;
    const targetEntry = context.targetEntry;

    logger.info(`Resolving conflict for entry: ${sourceEntry.dn}`);
    const resolvedEntry = resolveAttributeValueConflicts(sourceEntry, targetEntry);

    // Log conflict details to ticketing system
    const conflictDetails = JSON.stringify({ source: sourceEntry, target: targetEntry });
    logConflictToTicketingSystem(conflictDetails);

    context.targetEntry = resolvedEntry;
}

Conclusion

Automating conflict resolution in ForgeRock DS is crucial for maintaining data integrity and reducing manual overhead. By understanding your sync configurations, writing effective scripts, and testing thoroughly, you can streamline your identity management processes significantly. This saved me 3 hours last week, and I’m confident it will do the same for you.

Deploy your scripts, monitor the logs, and refine your conflict resolution strategy as needed. Happy coding!