APM Cookbook: Modify LDAP Attribute Values using iRulesLX
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.
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 } } }
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.
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.
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.
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”.
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
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.
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.
On the fallback branch after the Assign LDAP Variables object, add a iRule object.
Supply the following:
- Name: LDAP Modify
- ID: ldap_modify
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.
The finished policy should look similar to this:
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.
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.
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.
- gregan_306943Nimbostratus
Hi Brett,
When installing ldapjs i'm getting errors returned in the cli.
Active:In Sync] irule_ldap_extension npm install ldapjs -save npm ERR! Linux 2.6.32-431.56.1.el6. npm ERR! argv "/usr/bin/node" "/usr/bin/.npm__" "install" "ldapjs" "-save" npm ERR! node v0.12.15 npm ERR! npm v2.15.1 npm ERR! code EAI_AGAIN npm ERR! errno EAI_AGAIN npm ERR! syscall getaddrinfo
npm ERR! getaddrinfo EAI_AGAIN npm ERR! npm ERR! If you need help, you may report this error at: npm ERR! https://github.com/npm/npm/issues
- JaiASingla_2931Nimbostratus
Hi Gregan,
You will be fine if you make sure DNS is working on your unit and you have internet connectivity on the unit,
Thanks Jai
- forsanAltostratus
Hi,
I'm getting some errors when trying this out. Have you seen this before?
info sdmd[7077]: 018e0017:6: pid[25600] plugin[/Common/ldap_modify_plugin.ldap_modify_extension] dn: null,attr: employeeID,val: $CK$yeY8fCTj$mf5momsvuPKFH7MHQFlJPA==
info sdmd[7077]: 018e0017:6: pid[25600] plugin[/Common/ldap_modify_plugin.ldap_modify_extension] Creating LDAP Client.
info sdmd[7077]: 018e0017:6: pid[25600] plugin[/Common/ldap_modify_plugin.ldap_modify_extension] LDAP Bind Successful.
err sdmd[7077]: 018e0018:3: pid[25600] plugin[/Common/ldap_modify_plugin.ldap_modify_extension] /var/sdm/plugin_store/plugins/:Common:ldap_modify_plugin_45236_4/extensions/ldap_modify_extension/node_modules/ldapjs/lib/dn.js:171
err sdmd[7077]: 018e0018:3: pid[25600] plugin[/Common/ldap_modify_plugin.ldap_modify_extension] throw new TypeError('name (string) required');
err sdmd[7077]: 018e0018:3: pid[25600] plugin[/Common/ldap_modify_plugin.ldap_modify_extension] ^
err sdmd[7077]: 018e0018:3: pid[25600] plugin[/Common/ldap_modify_plugin.ldap_modify_extension] TypeError: name (string) required
err sdmd[7077]: 018e0018:3: pid[25600] plugin[/Common/ldap_modify_plugin.ldap_modify_extension] at Object.parse (/var/sdm/plugin_store/plugins/:Common:ldap_modify_plugin_45236_4/extensions/ldap_modify_extension/node_modules/ldapjs/lib/dn.js:171:11)
info sdmd[7077]: 018e0017:6: pid[25600] plugin[/Common/ldap_modify_plugin.ldap_modify_extension] MPI.0 ilx:45687:1 size 2
info sdmd[7077]: 018e000f:6: Extension /Common/ldap_modify_plugin:ldap_modify_extension pid 25600 exited with status 1
info sdmd[7077]: 018e000b:6: Extension /Common/ldap_modify_plugin:ldap_modify_extension started, pid:25626
info sdmd[7077]: 018e000f:6: Extension Unknown Extension pid 25600 exited with status 1 err sdmd[7077]: 018e0011:3: Received sigchld for unknown pid 25600
Br Andréas
- Slayer001Cirrus
I came across this when looking for a method to let the user reset their forgotten password. Is there a way to reset the unicodePwd attribute via this method. As I get plugin[/Common/plugin_ldap_modify.ldap_modify_extension] LDAP Modify Failed. when trying this.
I found some more information regarding the unicodePwd attribute (https://ldapwiki.com/wiki/Passwords%20Using%20LDIF):
"The modify request should contain a single replace operation with the new password enclosed in quotation marks and be Base64 encoded"
I added a base64 encoded value of the password between " marks but it still fails with the same Modify failed. and following error code: setup_io: it's not allowed to set the NT hash password directly
I noticed that unicodePwd has 2 colons behind it when it is changed via ldapmodify to indicate the base64 encoding. I'm not very familiar with javascript but I guess that's not included in the ldap_modify_extension?
- GymCirrus
I've successfully implemented what you're trying to do, as a proof of concept. You need to modify the index.js in the iLX workspace. For example (what I did), you could add the following function that encodes the value, then call the function IF the attribute name is unicodePwd.
// From https://github.com/ldapjs/node-ldapjs/issues/92#issuecomment-29070786 function encodePassword(password) { return new Buffer('"' + password + '"', 'utf16le').toString(); }
To call that function, I inserted the following line just after the three LDAP variables are declared:
if (ldap_attribute == 'unicodePwd') { ldap_value = encodePassword(ldap_value) }
- Scott_LarsonNimbostratus
on TMOS 13.1, I followed this example exactly, but I get an error right away when it's trying to parse the URL to create the LDAP client (note: this is an error constructing the objects (parsing the URL), not connecting to the server!)
Here are the errors that get logged (I removed the timestamp and other extraneous data from these log messages):
plugin[/Common/ldap_modify_plugin.ldap_modify_extension] Creating LDAP Client.
plugin[/Common/ldap_modify_plugin.ldap_modify_extension] /var/sdm/plugin_store/plugins/:Common:ldap_modify_plugin_118679_3/extensions/ldap_modify_extension/node_modules/ldapjs/lib/url.js:15
plugin[/Common/ldap_modify_plugin.ldap_modify_extension] throw new TypeError(urlStr + ' is an invalid LDAP url (scope)')
plugin[/Common/ldap_modify_plugin.ldap_modify_extension] TypeError: ldaps://10.1.30.101:636 is an invalid LDAP url (scope)
plugin[/Common/ldap_modify_plugin.ldap_modify_extension] at Object.parse (/var/sdm/plugin_store/plugins/:Common:ldap_modify_plugin_118679_3/extensions/ldap_modify_extension/node_modules/ldapjs/lib/url.js:15:13)
plugin[/Common/ldap_modify_plugin.ldap_modify_extension] at new Client (/var/sdm/plugin_store/plugins/:Common:ldap_modify_plugin_118679_3/extensions/ldap_modify_extension/node_modules/ldapjs/lib/client/client.js:114:33)
plugin[/Common/ldap_modify_plugin.ldap_modify_extension] at Object.createClient (/var/sdm/plugin_store/plugins/:Common:ldap_modify_plugin_118679_3/extensions/ldap_modify_extension/node_modules/ldapjs/lib/client/index.js:17:12)
plugin[/Common/ldap_modify_plugin.ldap_modify_extension] at Object.ldap_modify (/var/sdm/plugin_store/plugins/:Common:ldap_modify_plugin_118679_3/extensions/ldap_modify_extension/index.js:55:28)
plugin[/Common/ldap_modify_plugin.ldap_modify_extension] at ILXServerWrap.ilxServerMessageCallback [as onmessage] (/var/sdm/plugin_store/plugins/:Common:ldap_modify_plugin_118679_3/extensions/ldap_modify_extension/node_modules/f5-nodejs/lib/ilx_server.js:150:44)
Note: I looked at line 15 of url.js and here are the lines leading up to that and including line 15:
try {
parsedURL = new url.URL(urlStr)
} catch (error) {
throw new TypeError(urlStr + ' is an invalid LDAP url (scope)')
}
Note that although the error message says "(scope)", I do not believe the error lies with the scope since the example didn't even contain a scope. I think (scope) is a typo by the developer.
Anyone have any ideas?
- Bernhard_MNimbostratus
Hi, i have exactly the same error "TypeError: ldaps://10.1.30.101:636 is an invalid LDAP url (scope)" on my 15.1.2 installation.
EDIT:
I solved the problem by downgrading ldapjs node module to version 1.0.1.
npm install ldapjs@1.0.1 --no-bin-links --save
cheers, Bernhard
- MacNimbostratus
I am using this to modify AD. Everything works until there is a "comma" in the CN portion of the Distinguished Name. Has anyone encountered this issue? Any suggesitons on a fix?
Works
cn=doe jon, ou=example, dc=com
Fails
cn=doe, jon, ou=example, dc=com
SOLVED.
Added the following escape procedure
proc FixDN dn {
return [regsub -all {\\\,} $dn {\\,}]
}