#!/usr/bin/perl -w
#
# uniqueUidNumber
#
# Gets next unique uidNumber from sequence based on an entry in LDAP
#
# Andrew Findlay
#
# andrew.findlay@skills-1st.co.uk
#
# $Id$

package uniqueUidNumber;
use Exporter;
@EXPORT_OK = qw(getNextUidNumber setNextUidNumber);

use strict;
use Net::LDAP;
use Carp;

use vars qw($VERSION);
$VERSION = '0.1';

my $debug = 0;

# Limit times round the outer (read-update) loop
my $maxTries = 100;

# Limit times round the inner (add-n-and-update) loop
my $maxIncTries = 100;

# Add this to the trial UID value if we clash with an existing one
my $uidIncOnClash = 97;

=head1 NAME

uniqueUidNumber

=head1 DESCRIPTION

Provides a safe way to choose the next numericUID value when creating
entries for Posix users.

NOTE: getNextUidNumber depends on the LDAP server enforcing unique uidNumber values.

=head1 SYNOPSIS

	use uniqueUidNumber qw(getNextUidNumber setNextUidNumber);

	setNextUidNumber( $ldap, $entryDN, 123456 );

	...

	my $nextUid = getNextUidNumber( $ldap, $entryDN );

$ldap is a Net::LDAP object that has been bound to the LDAP server with permission
to modify the sequence entry.

$entryDN is the DN of an LDAP entry that will be used to keep track of the
next available uidNumber. The entry should look something like this:

	dn: cn=nextUID,dc=nis,dc=apps,dc=nis,dc=example,dc=org
	cn: nextUID
	objectclass: organizationalRole
	objectclass: posixAccount
	description: Holds the next free UID number
	uidNumber: 66000
	uid: nextUID
	gidNumber: 0
	homeDirectory: /no/such/place
	gecos: Dummy account holding the next free UID number

getNextUidNumber will try to work around problems
caused by people creating entries without having called getNextUidNumber
to choose the uidNumber. The value held in the sequence entry is guaranteed
to be available by the LDAP server uniqueness constraint on uidNumber.
If getNextUidNumber hits a constraint violation while trying to update
the sequence entry it assumes that someone has been allocating uidNumbers
by other means, adds 97 to the value it is trying to write back, and
tries again.

The last parameter to setNextUidNumber is a new value which will become the next
UID value to be handed out by getNextUidNumber().

=head1 ERROR HANDLING

getNextUidNumber will try several times to allocate a number.
If it fails, it will throw an error with die().

=cut

sub getNextUidNumber {
	my ($ldap, $seqBase) = @_;

	croak "getNextUidNumber: no sequencebase supplied" if not $seqBase;

	my $thisUidNumber;
	my $nextUidNumber;
	my $res;

	my $tries = $maxTries;
	while (1) {
		# Read the entry
		$res = $ldap->search( base => $seqBase, scope => 'base',
			filter => '(objectclass=*)', attrs => ['uidNumber']);

		$res->code && die "getNextUidNumber: Failed to read $seqBase (" . $res->error . ")";

		# Extract the current value that we are interested in
		my $entry = $res->pop_entry;
		$thisUidNumber = $entry->get_value('uidNumber');
		$nextUidNumber = $thisUidNumber+1;

		# Don't risk returning zero here...
		die "getNextUidNumber: No valid uidNumber found in $seqBase" if not $thisUidNumber;

		my $incTries = $maxIncTries;
		do {
			# Replace the old value as an atomic operation
			print "Updating sequence entry: $thisUidNumber -> $nextUidNumber\n" if $debug;
			$res = $ldap->modify( $seqBase,
					delete => { uidNumber => $thisUidNumber },
					add => { uidNumber => $nextUidNumber },
				);

			# All done if that worked...
			return $thisUidNumber if ($res->code == 0);

			# Dont loop forever - it hammers the LDAP server
			if ($incTries-- <= 0) {
				die "getNextUidNumber: Giving up after $maxIncTries attempts: check the value in $seqBase";
			}

			# Someone else got in first!
			printf "getNextUidNumber: Failed to get uidNumber %d (%s: %s). Trying again...\n", $nextUidNumber, $res->error_name, $res->error if $debug;

			# If the next number clashes then skip on further in case someone has loaded a heap of accounts without checking...
			$nextUidNumber += $uidIncOnClash if ($res->error_name eq "LDAP_CONSTRAINT_VIOLATION");

		} while ($res->error_name eq "LDAP_CONSTRAINT_VIOLATION");

		# Dont loop forever - it hammers the LDAP server
		if ($tries-- <= 0) {
			die "getNextUidNumber: Giving up after $maxTries attempts";
		}
	}

	return $thisUidNumber;
}

sub setNextUidNumber {
	my ($ldap, $seqBase, $nextUidNumber) = @_;

	my $res;

	# Overwrite whatever value is currently in the entry
	$res = $ldap->modify( $seqBase,
			replace => { uidNumber => ($nextUidNumber) },
		);

	die "getNextUidNumber: Cannot set new uidNumber in $seqBase (" . $res->error . ")" if $res->code;
}

1;

