Introduction

Access Policy Manager (APM) does not have the ability to modify LDAP attribute values using the native features of the product. In the past I’ve used some creative unsupported solutions to modify LDAP attribute values, but with the release of BIG-IP 12.1 and iRulesLX, you can now modify LDAP attribute values, in a safe and in supported manner.

Before we get started

I have a pre-configured Active Directory 2012 R2 server which I will be using as my LDAP server with an IP address of 10.1.30.101. My BIG-IP is running TMOS 12.1 and the iRules Language eXtension has been licensed and provisioned. Make sure your BIG-IP has internet access to download the required Node.JS packages. For this solution I’ve opted to use the ldapjs package.

The iRulesLX requires the following session variable to be set for the LDAP Modify to execute:

  • Distinguished Name (DN): session.ad.last.attr.dn
  • Attribute Name (e.g. carLicense): session.ldap.modify.attribute
  • Attribute Value: session.ldap.modify.value

This guide also assumes you have a basic level of understanding and troubleshooting at a Local Traffic Manager (LTM) level and you BIG-IP Self IP, VLANs, Routes, etc.. are all configured and working as expected.

Step 1 – iRule and iRulesLX Configuration

1.1 Create a new iRulesLX workspace

Local Traffic >> iRules >> LX Workspaces >> “Create”

Supply the following:

  • Name: ldap_modify_workspace

Select “Finished" to save.

You will now have any empty workspace, ready to cut/paste the TCL iRule and Node.JS code.

step1dot1

1.2 Add the iRule

Select “Add iRule” and supply the following:

  • Name: ldap_modify_apm_event
  • Select OK

Cut / Paste the following iRule into the workspace editor on the right hand side. Select “Save File” to save.

# Author: Brett Smith @f5

when RULE_INIT {
    # Debug logging control.
    # 0 = debug logging off, 1 = debug logging on.
    set static::ldap_debug 0
}

when ACCESS_POLICY_AGENT_EVENT {
    if { [ACCESS::policy agent_id] eq "ldap_modify" } {
        # Get the APM session data
        set dn [ACCESS::session data get session.ad.last.attr.dn]
        set ldap_attribute [ACCESS::session data get session.ldap.modify.attribute]
        set ldap_value [ACCESS::session data get session.ldap.modify.value]

        # Basic Error Handling - Don't execute Node.JS if LDAP attribute name or value is null    
        if { (([string trim $ldap_attribute] eq "") or ([string trim $ldap_value] eq "")) } {
            ACCESS::session data set session.ldap.modify.result 255
        } else {
            # Initialise the iRulesLX extension
            set rpc_handle [ILX::init ldap_modify_extension]
            if { $static::ldap_debug == 1 }{ log local0. "rpc_handle: $rpc_handle" }
       
            # Pass the LDAP Attribute and Value to Node.JS and save the iRulesLX response
            set rpc_response [ILX::call $rpc_handle ldap_modify $dn $ldap_attribute $ldap_value]
            if { $static::ldap_debug == 1 }{ log local0. "rpc_response: $rpc_response" }
            ACCESS::session data set session.ldap.modify.result $rpc_response
        }
    }
}

step1dot2

 

1.3 Add an Extension

Select “Add extenstion” and supply the following:

  • Name: ldap_modify_extension
  • Select OK

Cut / Paste the following Node.JS and replace the default index.js. Select “Save File” to save.

// Author: Brett Smith @f5
// index.js for ldap_modify_apm_events
 
// Debug logging control.
// 0 = debug off, 1 = debug level 1, 2 = debug level 2
var debug = 1;
 
// Includes
var f5 = require('f5-nodejs');
var ldap = require('ldapjs');

// Create a new rpc server for listening to TCL iRule calls.
var ilx = new f5.ILXServer(); 
 
// Start listening for ILX::call and ILX::notify events.
ilx.listen();

// Unbind LDAP Connection
function ldap_unbind(client){
    client.unbind(function(err) {
        if (err) {
            if (debug >= 1) { console.log('Error Unbinding.'); }
        } else {
            if (debug >= 1) { console.log('Unbind Successful.'); }
        }
    });
}

// LDAP Modify method, requires DN, LDAP Attribute Name and Value
ilx.addMethod('ldap_modify', function(ldap_data, response) {
   
    // LDAP Server Settings
    var bind_url = 'ldaps://10.1.30.101:636';
    var bind_dn = 'CN=LDAP Admin,CN=Users,DC=f5,DC=demo';
    var bind_pw = 'Password123';
   
    // DN, LDAP Attribute Name and Value from iRule
    var ldap_dn = ldap_data.params()[0];
    var ldap_attribute = ldap_data.params()[1];
    var ldap_value = ldap_data.params()[2];
   
    if (debug >= 2) { console.log('dn: ' + ldap_dn + ',attr: ' + ldap_attribute + ',val: ' + ldap_value); }
   
    var ldap_modification = {};
    ldap_modification[ldap_attribute] = ldap_value;

    var ldap_change = new ldap.Change({
        operation: 'replace',
        modification: ldap_modification
    });

    if (debug >= 1) { console.log('Creating LDAP Client.'); }
   
    // Create LDAP Client
    var ldap_client = ldap.createClient({
        url: bind_url,
        tlsOptions: { 'rejectUnauthorized': false } // Ignore Invalid Certificate - Self Signed etc..
    });

    // Bind to the LDAP Server
    ldap_client.bind(bind_dn, bind_pw, function(err) {
        if (err) {
            if (debug >= 1) { console.log('Error Binding to: ' + bind_url); }
            response.reply('1'); // Bind Failed
            return;
        } else {
            if (debug >= 1) { console.log('LDAP Bind Successful.'); }
            // LDAP Modify
            ldap_client.modify(ldap_dn, ldap_change, function(err) {
                if (err) {
                    if (debug >= 1) { console.log('LDAP Modify Failed.'); }
                    ldap_unbind(ldap_client);
                    response.reply('2'); // Modify Failed
                } else {
                    if (debug >= 1) { console.log('LDAP Modify Successful.'); }
                    ldap_unbind(ldap_client);
                    response.reply('0'); // No Error
                }
            });
        }
    });
});

You will need to modify the bind_url, bind_dn, and bind_pw variables to match your LDAP server settings.

step1dot3

1.4 Install the ldapjs package

  • SSH to the BIG-IP as root
  • cd /var/ilx/workspaces/Common/ldap_modify_workspace/extensions/ldap_modify_extension
  • npm install ldapjs -save

You should expect the following output from above command:

[root@big-ip1:Active:Standalone] ldap_modify_extension # npm install ldapjs -save

ldapjs@1.0.0 node_modules/ldapjs
├── assert-plus@0.1.5
├── dashdash@1.10.1
├── asn1@0.2.3
├── ldap-filter@0.2.2
├── once@1.3.2 (wrappy@1.0.2)
├── vasync@1.6.3
├── backoff@2.4.1 (precond@0.2.3)
├── verror@1.6.0 (extsprintf@1.2.0)
├── dtrace-provider@0.6.0 (nan@2.4.0)
└── bunyan@1.5.1 (safe-json-stringify@1.0.3, mv@2.1.1)

1.5 Create a new iRulesLX plugin

Local Traffic >> iRules >> LX Plugin >> “Create”

Supply the following:

  • Name: ldap_modify_plugin
  • From Workspace: ldap_modify_workspace

Select “Finished" to save.

step1dot4

 

If you look in /var/log/ltm, you will see the extension start a process per TMM for the iRuleLX plugin.

big-ip1 info sdmd[6415]: 018e000b:6: Extension /Common/ldap_modify_plugin:ldap_modify_extension started, pid:24396
big-ip1 info sdmd[6415]: 018e000b:6: Extension /Common/ldap_modify_plugin:ldap_modify_extension started, pid:24397
big-ip1 info sdmd[6415]: 018e000b:6: Extension /Common/ldap_modify_plugin:ldap_modify_extension started, pid:24398
big-ip1 info sdmd[6415]: 018e000b:6: Extension /Common/ldap_modify_plugin:ldap_modify_extension started, pid:24399

 

Step 2 – Create a test Access Policy

2.1 Create an Access Profile and Policy

We can now bring it all together using the Visual Policy Editor (VPE). In this test example, I will not be using a password just for simplicity.

Access Policy >> Access Profiles >> Access Profile List >> “Create”

Supply the following:

  • Name: ldap_modify_ap
  • Profile Type: LTM-APM
  • Profile Scope: Profile
  • Languages: English (en)
  • Use the default settings for all other settings.

Select “Finished” to save.

step2dot1_1

step2dot1_2

 

2.2 Edit the Access Policy in the VPE

Access Policy >> Access Profiles >> Access Profile List >> “Edit” (ldap_modify_ap)

On the fallback branch after the Start object, add a Logon Page object.

Change the second field to:

  • Type: text
  • Post Variable Name: attribute
  • Session Variable Name: attribute
  • Read Only: No

Add a third field:

  • Type: text
  • Post Variable Name: value
  • Session Variable Name: value
  • Read Only: No

In the “Customization” section further down the page, set the “Form Header Text” to what ever you like and change “Logon Page Input Field #2” and “Logon Page Input Field #3”  to something meaningful, see my example below for inspiration. Leave the “Branch Rules” as the default. Don’t forget to “Save”.

step2dot2

 

On the fallback branch after the Logon Page object, add an AD Query object.

This step verifies the username is correct against Active Directory/LDAP, returns the Distinguished Name (DN) and stores the value in session.ad.last.attr.dn which will be used by the iRulesLX.

Supply the following:

  • Server: Select your LDAP or AD Server
  • SearchFilter: sAMAccountName=%{session.logon.last.username}
  • Select Add new entry
  • Required Attributes:  dn

step2dot3

Under Branch Rules, delete the default and add a new one, by selecting Add Branch Rule.

Update the Branch Rule settings:

Name: AD Query Passed

Expression (Advanced): expr { [mcget {session.ad.last.queryresult}] == 1 }

Select “Finished”, then “Save” when your done.

step2dot4

step2dot4_2

 

On the AD Query Passed branch after the AD Query object, add a Variable Assign object.

This step assigns the Attribute Name to session.ldap.modify.attribute and the Attribute Value entered on the Logon Page to session.ldap.modify.value.

Supply the following:

  • Name: Assign LDAP Variables

Add the Variable assignments by selecting Add new entry >> change.

Variable Assign 1:

  • Custom Variable (Unsecure): session.ldap.modify.attribute
  • Session Variable: session.logon.last.attribute

Variable Assign 2:

  • Custom Variable (Secure): session.ldap.modify.value
  • Session Variable: session.logon.last.value

Select “Finished”, then “Save” when your done.Leave the “Branch Rules” as the default.

step2dot5

 

On the fallback branch after the Assign LDAP Variables object, add a iRule object.

Supply the following:

  • Name: LDAP Modify
  • ID: ldap_modify

step2dot6_1

Under Branch Rules, add a new one, by selecting Add Branch Rule.

Update the Branch Rule settings:

Name: LDAP Modify Successful

Expression (Advanced): expr { [mcget {session.ldap.modify.result}] == "0" }

Select “Finished”, then “Save” when your done.

step6_2

 

The finished policy should look similar to this:

VPE

 

As this is just a test policy I used to test my Node.JS and to show how the LDAP Modify works, I will not have a pool member attached to the virtual server, I have just left the branch endings as Deny. In a real word scenario, you would not allow a user to change any LDAP Attributes and Values.

Apply the test Access Policy (ldap_modify_ap) to a HTTPS virtual server for testing and the iRuleLX under the Resources section.

vs_resources

 

Step 3 - OK, let’s give this a test!

To test, just open a browser to the HTTPS virtual server you created, and supply a Username, Attribute and Value to be modified. In my example, I want to change the Value of the carLicense attribute to test456.

logon_page

 

Prior to me hitting the Logon button, I did a ldapsearch from the command line of the BIG-IP:

ldapsearch -x -h 10.1.30.101 -D "cn=LDAP Admin,cn=users,dc=f5,dc=demo" -b "dc=f5,dc=demo" -w 'Password123' '(sAMAccountName=test.user)' | grep carLicense
carLicense: abc123

Post submission, I performed the same ldapsearch and the carLicense value has changed. It works!

ldapsearch -x -h 10.1.30.101 -D "cn=LDAP Admin,cn=users,dc=f5,dc=demo" -b "dc=f5,dc=demo" -w 'Password123' '(sAMAccountName=test.user)' | grep carLicense
carLicense: test456

Below is some basic debug log from the Node.JS:

big-ip1 info sdmd[6415]: 018e0017:6: pid[24399] plugin[/Common/ldap_modify_plugin.ldap_modify_extension] Creating LDAP Client.
big-ip1 info sdmd[6415]: 018e0017:6: pid[24399] plugin[/Common/ldap_modify_plugin.ldap_modify_extension] LDAP Bind Successful.
big-ip1 info sdmd[6415]: 018e0017:6: pid[24399] plugin[/Common/ldap_modify_plugin.ldap_modify_extension] LDAP Modify Successful.
big-ip1 info sdmd[6415]: 018e0017:6: pid[24399] plugin[/Common/ldap_modify_plugin.ldap_modify_extension] Unbind Successful.

 

Conclusion

You can now modify LDAP attribute values, in safe and in supported manner with iRulesLX. Think of the possibilities!

For an added bonus, you can add addtional branch rules to the iRule Event - LDAP Modify, as the Node.JS returns the following error codes:

1 - LDAP Bind Failed

2 - LDAP Modified Failed

I would also recommend using Macros.

Please note, this my own work and has not been formally tested by F5 Networks.