I'm writing a user management system in LDAP. It is pretty simple: allows non-technical users to add new accounts for the mail server, edit some details of existing accounts, a few other management functions. Simple stuff... well, it ought to be, anyway, but some bits are waaaay harder than I expected.
Here's an example: each new user who is added is also a posixAccount - this means they need a unique uidNumber. For example, bbuilder has uidNumber 664, sfireman has uidNumber 665, and so on. Two users having the same UID would be A Bad Thing. If this was using a database, the problem is easily solved:
CREATE SEQUENCE nextUidNumber;
CREATE TABLE users (uidNumber DEFAULT nextval('nextUidNumber'), ... );
For good measure, enforce uniqueness with a unique index on users(uidNumber).
LDAP makes this kind of thing rather harder.
First, you need a single-valued attribute to hold your next UID number. Amazingly, I cannot find a pre-existing schema for this - surely it's the kind of thing people need to do regularly. So I had to create my own schema. First, I had to get a Private Enterprise Number (PEN) assigned from IANA so my schema would be in its own space (known as an arc in LDAP and SNMP speak).
Then I set to writing a simple schema. The OpenLDAP doco was excellent on this topic. Nevertheless it took me a few goes to get it working to my satisfaction.
The schema I came up with looks like this:
objectIdentifier ROVOID 1.3.6.1.4.1.<PEN> # not actually our PEN, I'm still waiting for it
objectIdentifier ROVSNMP ROVOID:1
objectIdentifier ROVLDAP ROVOID:2
objectIdentifier ROVLDAPATTR ROVLDAP:1
objectIdentifier ROVLDAPOBJECT ROVLDAP:2
attributeType ( ROVLDAPATTR:1 NAME 'x-rov-nextUidNumber'
DESC 'A counter for storing next UID number for posixAccounts'
EQUALITY integerMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27{32768}
SINGLE-VALUE )
objectClass ( ROVLDAPOBJECT:1 NAME 'x-rov-UidCounter'
DESC 'objectClass containing the next UID counter'
SUP top STRUCTURAL
MUST ( x-rov-nextUidNumber $ name ) )
This was created as file /etc/ldap/schema/local.schema, then added to the list of imported schemas in /etc/ldap/slapd.conf then re-start slapd.
Then create an instance of this objectClass (I've stashed ours in name=mailAccountUid,ou=IT,ou=mgmt - a place reserved for management info for the application). I cheated and used phpLdapAdmin, but the LDIF would look like this:
dn: name=mailAccountUid,ou=IT,ou=mgmt,dc=example,dc=com,dc=au
name: mailAccountUid
objectClass: x-rov-UidCounter
objectClass: top
x-rov-nextUidNumber: 1
You also want to set nextUidNumber to the start of the range you're using for your LDAP user accounts (you do not to want to start at 1 - typically, these would be used by system accounts: check the output of `getent passwd` to see which are already in use. I chose to start at a nice arbitrary 10,000:
dn: name=mailAccountUid,ou=IT,ou=mgmt,dc=example,dc=com,dc=au
changetype: modify
replace: x-rov-nextUidNumber
x-rov-nextUidNumber: 10000
Now when you want the next available UID, you can get the value of x-rov-nextUidNumber, increment by one, then write it back to the LDAP store. Of course, then you have the issue of concurrency - what happens if two users are added at the same time?
Various solutions have been suggested, but the one I have used (and tested - it works!) is to do an atomic modify operation consisting of delete and add for the attribute. It works because if you attempt to delete the attribute with the value you read, and it throws a NoSuchAttributeException, you know someone else messed with it while you were incrementing it and trying to write it back, so you just try again - get, increment, write it back. I've put this in a loop of 5 attempts. I think this is reasonable for a company with about 200 users. I'm working in java for this, but the same atomic modification should be available via other language APIs. In JNDI it is DirContext.modifyAttributes(String, ModificationItem[]), and you use it like this:
public static int getNextUidNumber(DirContext ctx) throws NamingException
{
String fnName = "getNextUidNumber: ";
Utils.debug(fnName + "start");
int retval = 0;
int numAttempts = 5; // how many times we'll try to atomically get next UID before giving up
Attributes matchAttrs = new BasicAttributes(true); // ignore attribute name case
matchAttrs.put(new BasicAttribute("objectClass", "x-rov-UidCounter"));
matchAttrs.put(new BasicAttribute("name", "mailAccountUid"));
String[] returnAttrs = { "x-rov-nextUidNumber" };
for (int i = 0; i < numAttempts; i++)
{
try
{
NamingEnumeration answer = ctx.search("ou=IT,ou=mgmt,dc=example,dc=com,dc=au", matchAttrs, returnAttrs);
SearchResult sr = (SearchResult) answer.next();
Attributes attrs = sr.getAttributes();
String thing = safeStringGet(attrs, "x-rov-nextUidNumber");
Utils.debug(fnName + "got answer [" + thing + "]");
retval = Integer.parseInt(thing);
int nextval = retval + 1;
Utils.debug(fnName + "replacing with [" + nextval + "]");
ModificationItem removeOld = new ModificationItem(DirContext.REMOVE_ATTRIBUTE, new BasicAttribute("x-rov-nextUidNumber", "" + retval));
ModificationItem addNew = new ModificationItem(DirContext.ADD_ATTRIBUTE, new BasicAttribute("x-rov-nextUidNumber", "" + nextval));
ModificationItem[] atomicReplace = {removeOld, addNew};
ctx.modifyAttributes("name=mailAccountUid,ou=IT,ou=mgmt,dc=example,dc=com,dc=au", atomicReplace);
return retval;
}
catch (NoSuchAttributeException nsaex)
{
Utils.info(fnName+"exception on atomic increment, trying again: " + nsaex);
}
}
throw new NamingException("Could not get next UID after " + numAttempts + " attempts");
}
For the sake of completeness, here is the definition for safeStringGet - a convenience function to avoid having to null-check the attribute before attempting to get its value.
private static String safeStringGet(Attributes attr, String attrName) throws NamingException
{
Attribute at = attr.get(attrName);
if (at != null)
{
return (String) at.get();
}
return null;
}
There you have it - a couple of days of work (coding, reading, coding some more, asking questions on LDAP lists, more reading, more coding) to achieve something that I think should have been easy.
My thanks go to Vincent Ryan for explaining how to use modifyAttributes() to achieve an atomic operation. He also warns that the atomicity only holds for single-master LDAP set-up - with multi-masters, this atomicity is not guaranteed. Since I'm not game to go within a million miles of a multi-master setup, and have no need to anyway, I'm noting this for the sake of completeness and moving on.
Thanks also to Francis Swasey who sent me Perl code to do the same in perl-ldap
my $umsg = $ld->modify($dn,
delete => { 'uidNumber' => $currentvalue },
add => { 'uidNumber' => $nextvalue});
(where $currentvalue is the value I just retrieved from my OpenLDAP server and $nextvalue is $currentvalue incremented by 1).
No comments:
Post a Comment