Introduction
This project demonstrates how a system administrator could dynamically create organizational units in any LDAP directory using the "scripted" provisioning of MIIS 2003/ILM 2007.
Background
System administrators often face the task of creating a OU structure in a corporate LDAP directory, such as Active Directory, ADAM/ADLDS, OpenLDAP, etc. In an organization where the administrator is asked to place a user account object in the OU corresponding to the user's department, title, or any other dynamically calculated container based on the user's attributes, (s)he must know (and therefore hardcode) values of target containers/organizational units in the LDAP connected directory in question.
MIIS 2003/ILM 2007 developer reference is rich with examples of placing user account within a pre-defined OU based on the OU's name. In the event when the parent OU is not available, the administrator is expected to create an organizational unit object manually. In the same time, should the organization extend the list of departments (and therefore the list of the corresponding OUs), the provisioning code will have to be augmented to include the new values (path) and provisioning/de-provisioning business logic for the newly added target OUs.
To avoid this practice of re-compiling of provisioning code for every adjustment in the organizational structure of an enterprise, the administrator could implement a mechanism to create parent organizational units dynamically based on the attribute values of the user object in the Metaverse.
This code example also provides a clear path for de-provisioning a user account in future. To illustrate the challenge of dynamic provisioning of OUs based on the "user" object-type provisioning cycle, we will need to understand the initial provisioning logic of the first user account that encounteres the condition where the parent OU is missing. The code will "detect" that parent OU is missing and it will generate a CSEntry
object of "organizationalUnit
" type in the target management agent. Consequently, the organizational unit object will become (and remain) connected to the user (person) MVEntry
object. All consecutive provisioning attempts of any other user object to the previously dynamically-generated OU will be successful. However, a problem could arise when the "first" user in this dynamically generated OU is ready to be de-provisioned. Since the OU object is still connected to that user object, the de-provisioning routine could de-provision the organizational unit object along with the user object, which will leave all other users provisioned to the same OU without a "parent". To avoid this unwanted condition, the provided code example disconnects an "organizationalUnit
" object from a "user" object during the next synchronization cycle of the Sync Engine.
It is important to make sure that your configuration is not set to leave disconnectors of the "organizationalUnit
" type as "normal disconnector".
Administrators are strongly encouraged to review de-provisioning logic for all types of objects while implementing this dynamic OU provisioning routine.
Happy coding!
Using the Code
To use this code, you will need to download the source and compile it as your "provisioning" DLL. This code implements the Microsoft.MetadirectoryServices.IMVSynchronization
interface. Most of the business logic is implemented in the Provision(MVEntry)
method.
[assembly:System.CLSCompliant(true)]
[assembly:System.Runtime.InteropServices.ComVisible(false)]
namespace Mms_Metaverse
{
using System;
using System.Collections.Generic;
using System.Globalization;
using Microsoft.MetadirectoryServices;
public class MVExtensionObject : IMVSynchronization
{
private const string TargetMAName = "ADMA";
private List<ReferenceValue> failedObjects;
public MVExtensionObject()
{
this.failedObjects = new List<ReferenceValue>();
}
private enum CSEntryObjectType
{
user,
organizationalUnit
}
private enum MVEntryObjectType
{
person,
organizationalUnit
}
#region Interface implementation
void IMVSynchronization.Initialize()
{
}
void IMVSynchronization.Provision(MVEntry mventry)
{
DisjoinOrganizationalUnits(mventry);
this.ExecuteProvisioning(mventry);
}
bool IMVSynchronization.ShouldDeleteFromMV(CSEntry csentry, MVEntry mventry)
{
throw new EntryPointNotImplementedException();
}
void IMVSynchronization.Terminate()
{
this.failedObjects = null;
}
#endregion Interface implementation
private static bool CanProvision(MVEntry mventry, MVEntryObjectType expectedMVObjectType)
{
if (!mventry.ObjectType.Equals(Enum.GetName(
typeof(MVEntryObjectType), expectedMVObjectType),
StringComparison.OrdinalIgnoreCase))
{
return false;
}
switch (expectedMVObjectType)
{
case MVEntryObjectType.person:
{
if (!mventry["givenName"].IsPresent ||
!mventry["sn"].IsPresent ||
!mventry["department"].IsPresent ||
!mventry["title"].IsPresent)
{
return false;
}
return true;
}
default:
{
return false;
}
}
}
private static bool ShouldProvision(MVEntry mventry,
MVEntryObjectType expectedMVObjectType,
CSEntryObjectType expectedCSObjectType)
{
if (!IsExpectedObjectType(mventry, expectedMVObjectType))
{
return false;
}
switch (expectedMVObjectType)
{
case MVEntryObjectType.person:
{
byte i = 0;
foreach (CSEntry csentry in mventry.ConnectedMAs[TargetMAName].Connectors)
{
if (IsExpectedObjectType(csentry, expectedCSObjectType))
{
i++;
}
}
return i.Equals(0);
}
default:
{
return false;
}
}
}
private static bool ShouldRename(ConnectedMA target,
ReferenceValue distinguishedName, MVEntryObjectType expectedMVObjectType)
{
switch (expectedMVObjectType)
{
case MVEntryObjectType.person:
{
CSEntry[] entries = GetCSEntryObjects(target, CSEntryObjectType.user);
if (!entries.Length.Equals(1))
{
throw new UnexpectedDataException("This provisioning" +
" code cannot support multiple connectors scenario(s)");
}
return !entries[0].DN.Equals(distinguishedName);
}
default:
{
throw new UnexpectedDataException("This provisioning code" +
" cannot support object type " +
Enum.GetName(typeof(MVEntryObjectType), expectedMVObjectType));
}
}
}
private static bool IsExpectedObjectType(MVEntry mventry,
MVEntryObjectType expectedObjectType)
{
return mventry.ObjectType.Equals(Enum.GetName(typeof(MVEntryObjectType),
expectedObjectType), StringComparison.OrdinalIgnoreCase);
}
private static bool IsExpectedObjectType(CSEntry csentry,
CSEntryObjectType expectedObjectType)
{
return csentry.ObjectType.Equals(Enum.GetName(typeof(CSEntryObjectType),
expectedObjectType), StringComparison.OrdinalIgnoreCase);
}
private static void DisjoinOrganizationalUnits(MVEntry mventry)
{
if (!IsExpectedObjectType(mventry, MVEntryObjectType.person))
{
return;
}
foreach (CSEntry csentry in
GetCSEntryObjects(mventry.ConnectedMAs[TargetMAName],
CSEntryObjectType.organizationalUnit))
{
csentry.Deprovision();
}
}
private static CSEntry[] GetCSEntryObjects(ConnectedMA target,
CSEntryObjectType expectedObjectType)
{
List<CSEntry> entries = new List<CSEntry>();
foreach (CSEntry csentry in target.Connectors)
{
if (IsExpectedObjectType(csentry, expectedObjectType))
{
entries.Add(csentry);
}
}
return entries.ToArray();
}
private static ReferenceValue GetDistinguishedName(MVEntry mventry,
MVEntryObjectType expectedObjectType)
{
switch (expectedObjectType)
{
case MVEntryObjectType.person:
{
string commonName = GetCommonName(mventry);
string container = GetContainer(mventry);
return mventry.ConnectedMAs[TargetMAName].
EscapeDNComponent(commonName).Concat(container);
}
default:
{
throw new UnexpectedDataException("Cannot process object type " +
Enum.GetName(typeof(MVEntryObjectType), expectedObjectType));
}
}
}
private static string GetCommonName(MVEntry mventry)
{
return string.Format(CultureInfo.InvariantCulture, "CN={0} {1}",
mventry["givenName"].StringValue, mventry["sn"].StringValue);
}
private static string GetContainer(MVEntry mventry)
{
string domainName = string.Format(CultureInfo.InvariantCulture,
@"OU=People,DC=contoso,DC=com");
return string.Format(CultureInfo.InvariantCulture, "OU={0},OU={1},{2}",
mventry["title"].StringValue,
mventry["department"].StringValue, domainName);
}
private void CreateParentOrganizationalUnitObject(ConnectedMA target,
ReferenceValue distinguishedName)
{
ReferenceValue currentDistingusghedName = distinguishedName.Parent();
CSEntry csentry = target.Connectors.StartNewConnector(Enum.GetName(
typeof(CSEntryObjectType), CSEntryObjectType.organizationalUnit));
try
{
csentry.DN = currentDistingusghedName;
csentry.CommitNewConnector();
this.OnSuccessfullyCommittedObject(csentry.DN);
}
catch (MissingParentObjectException)
{
this.OnMissingParentObject(currentDistingusghedName);
this.CreateParentOrganizationalUnitObject(target, currentDistingusghedName);
}
}
private void CreateUserObject(ConnectedMA target, ReferenceValue distinguishedName)
{
CSEntry csentry = target.Connectors.StartNewConnector(
Enum.GetName(typeof(CSEntryObjectType), CSEntryObjectType.user));
csentry.DN = distinguishedName;
csentry.CommitNewConnector();
this.OnSuccessfullyCommittedObject(csentry.DN);
}
private void ExecuteProvisioning(MVEntry mventry)
{
MVEntryObjectType objectType = (MVEntryObjectType)Enum.Parse(
typeof(MVEntryObjectType), mventry.ObjectType, true);
switch (objectType)
{
case MVEntryObjectType.person:
{
bool shouldProvisionUser = ShouldProvision(mventry,
MVEntryObjectType.person, CSEntryObjectType.user);
bool canProvisionUser = CanProvision(mventry, MVEntryObjectType.person);
if (!canProvisionUser)
{
return;
}
ReferenceValue distinguishedName =
GetDistinguishedName(mventry, MVEntryObjectType.person);
bool shouldRename = false;
ConnectedMA target = mventry.ConnectedMAs[TargetMAName];
if (!shouldProvisionUser)
{
shouldRename = ShouldRename(target, distinguishedName,
MVEntryObjectType.person);
}
try
{
if (shouldProvisionUser && canProvisionUser)
{
this.CreateUserObject(target, distinguishedName);
}
if (shouldRename)
{
this.RenameUserObject(target, distinguishedName);
}
}
catch (MissingParentObjectException)
{
this.OnMissingParentObject(distinguishedName);
this.CreateParentOrganizationalUnitObject(target,
distinguishedName);
}
while (this.failedObjects.Count != 0)
{
this.ExecuteProvisioning(mventry);
}
break;
}
default:
{
return;
}
}
}
private void OnMissingParentObject(ReferenceValue failedObject)
{
if (!this.failedObjects.Contains(failedObject))
{
this.failedObjects.Add(failedObject);
}
}
private void OnSuccessfullyCommittedObject(ReferenceValue succeededObject)
{
if (this.failedObjects.Contains(succeededObject))
{
this.failedObjects.Remove(succeededObject);
}
}
private void RenameUserObject(ConnectedMA target, ReferenceValue distinguishedName)
{
CSEntry csentry = target.Connectors.ByIndex[0];
csentry.DN = distinguishedName;
this.OnSuccessfullyCommittedObject(csentry.DN);
}
}
}
Please send comments/suggestions