Jorge's Quest For Knowledge!

All You Need To Know About Identity And Security On-Premises And In The Cloud. It's Just Like An Addiction, The More You Have, The More You Want To Have!

(2009-05-05) Dynamically Creating Not Yet Existing OUs During (User) Object Provisioning

Posted by Jorge on 2009-05-05


Whenever you are provisioning into Active Directory (or any other LDAP directory), it is normal to perform a discovery import, so that you have the containers/OUs (e.g. based upon ) into which you will be placing users (and groups etc.). If you do not do this, when objects are provisioned into the connector space representing that LDAP directory an error is thrown like "parent is missing". Let’s say you do import existing containers/OUs, but you need to provision objects in a not yet existing OU. What do you do? You either manually created the OU manually after you see the error or even before the actual run of the provisioning/synchronization engine to prevent the error. But if an OU structure is based upon DEPARTMENT and/or LOCATION attribute values on user objects you might want to have this done automatically by catching the error "MissingParentException" and provision the missing OUs (recursive for all parent objects until complete DN is valid) first prior to provisioning the actual (user) object. The catch with this is that the MV object representing the user object is connected to the provisioned user object in the connector space representing the LDAP directory AND all OUs that were also pre-provisioned because these did not yet exist. This only applies to the very first user object that generates the "MissingParentException" error and not for subsequent user objects that have the same parent OU. When the original OU is deprovisioned all connected objects in the connector space representing the LDAP directory (including the OUs) are deprovisioned. That’s something you do not want to happen and in the next run of the provisioning/synchronization engine you need to disconnect the OUs in the connector space representing the LDAP directory from the user object in the metaverse.

Dmitry Kazantsev provides a piece of code in C# that achieves the automatic procedure as described above. That code and additional information can be found here (Dynamic Organizational Unit provisioning with ILM 2007/MIIS 2003). You can use the link, but you can also read the information below which was copied from CodeProject. All credits go to Dmitry Kazantsev.

——————————

Introduction

This project demonstrates how system administrator could dynamically create organizational units in any LDAP directory using "scripted" provisioning of MIIS 2003/ILM 2007

Background

System administrators are often facing task of creating OU structure in a corporate LDAP directory, such as Active Directory, ADAM/ADLDS, OpenLDAP, etc. In the organization where administrator is asked to place user account object in the OU corresponding to 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 pre-defined OU based on the OU’s name. In the event when parent OU if not available administrator is expected to create an organizational unit object manually. In the same time, should organization extend list of the departments (and therefore list of the corresponding OUs), the provisioning code will have to be augmented to include new values (path) and provisioning/de-provisioning business logic for newly added target OUs. To avoid this practice of re-compiling of provisioning code for every adjustment in the organizational structure of an enterprise 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 clear path for de-provisioning of the user account in the future. To illustrate 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 encountered the condition where parent OU was missing. Code will "detect" that parent OU is missing and it will generate the 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 objects to the previously dynamically-generated OU will be successful. However problem could arise when "first" user in this dynamically generated OU is ready to be de-provisioned. Since the OU object is still connected to that user object 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 provided code example disconnects an "organizationalUnit" object from "user" object during 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 source and compile it as your "provisioning" dll; This code implements Microsoft.MetadirectoryServices.IMVSynchronization interface. Most of the business login is implemented in Provision(MVEntry) method.

If it hurts your eyes to see the code below, you can also download it from here. J

//----------------------------------------------------------------------- // <copyright file="MV.DynamicOUProvisioning.cs" company="LostAndFoundIdentity"> // Copyright (c) LostAndFoundIdentity.com. All rights reserved. // </copyright> // <author>Dmitry Kazantsev</author> //----------------------------------------------------------------------- [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; /// <summary> /// Implements IMVSynchronization interface /// </summary> public class MVExtensionObject : IMVSynchronization { /// <summary> /// Variable containing name of the target management agent. This string must match the name of your target LDAP directory management agent /// </summary> private const string TargetMAName = "ADMA"; /// <summary> /// The collection of the "missing" objects /// </summary> private List<ReferenceValue> failedObjects; /// <summary> /// Initializes a new instance of the MVExtensionObject class /// </summary> public MVExtensionObject() { this.failedObjects = new List<ReferenceValue>(); } /// <summary> /// CSEntry object type enumeration /// </summary> private enum CSEntryObjectType { /// <summary> /// Represents "User" object type of Active Directory /// </summary> user, /// <summary> /// Represents "Organizational Unit" object type of Active Directory /// </summary> organizationalUnit } /// <summary> /// MVEntry object type enumeration /// </summary> private enum MVEntryObjectType { /// <summary> /// Represents "Person" object type of the Metaverse /// </summary> person, /// <summary> /// Represents "Organizational Unit" object type of the Metaverse /// </summary> organizationalUnit } #region Interface implementation /// <summary> /// Implements IMVSynchronization.Initialize method /// </summary> void IMVSynchronization.Initialize() { } /// <summary> /// Implements IMVSynchronization.Provision method /// </summary> /// <param name="mventry">The MVEntry object in question</param> void IMVSynchronization.Provision(MVEntry mventry) { //// ATTENTION: Add call to cutom object de-provisioning method here if/when needed DisjoinOrganizationalUnits(mventry); this.ExecuteProvisioning(mventry); } /// <summary> /// Implements IMVSynchronization.ShouldDeleteFromMV method /// </summary> /// <param name="csentry">The CSEntry object in question</param> /// <param name="mventry">The MVEntry object in question</param> /// <returns>Boolean value representing whether the object should be deleted from the metaverse</returns> bool IMVSynchronization.ShouldDeleteFromMV(CSEntry csentry, MVEntry mventry) { throw new EntryPointNotImplementedException(); } /// <summary> /// Implements IMVSynchronization.Terminate method /// </summary> void IMVSynchronization.Terminate() { this.failedObjects = null; } #endregion Interface implementation /// <summary> /// Determins whether the object of the given type can be provisioned based on the presence of the "must have" attributes /// </summary> /// <param name="mventry">The MVEntry object in question</param> /// <param name="expectedMVObjectType">The desierd type of the MVEntry object</param> /// <returns>Boolean value representing whether the object can be provisined into the target management agent</returns> private static bool CanProvision(MVEntry mventry, MVEntryObjectType expectedMVObjectType) { //// Return 'false' when the object type of the provided MVEntry dosent match the provided desierd object type if (!mventry.ObjectType.Equals(Enum.GetName(typeof(MVEntryObjectType), expectedMVObjectType), StringComparison.OrdinalIgnoreCase)) { return false; } //// Pick the object type in question //// TODO: Extend this 'switch' with more cases for object types, when/if needed switch (expectedMVObjectType) { case MVEntryObjectType.person: { //// Verify for all "must-have" attributes //// TODO: Add any other pre-requirements for successful provisioning here if (!mventry["givenName"].IsPresent || !mventry["sn"].IsPresent || !mventry["department"].IsPresent || !mventry["title"].IsPresent) { //// All conditions are met - returning 'true' return false; } //// Some conditions are not satisfied - returning 'false' return true; } default: { //// This object type is not described //// TODO: Extend this 'switch' with more cases for object types, when/if needed return false; } } } /// <summary> /// Determines whether the object in question should be provisioned into the target management agent /// </summary> /// <param name="mventry">MVEntry in question</param> /// <param name="expectedMVObjectType">Expected 'source' MVEntry object type</param> /// <param name="expectedCSObjectType">Expected 'target' CSEntry object type </param> /// <returns>Boolean value representing whether the object should be provisioned into the target management agent</returns> private static bool ShouldProvision(MVEntry mventry, MVEntryObjectType expectedMVObjectType, CSEntryObjectType expectedCSObjectType) { //// ATTENTION: Adjust business logic to describe whether object should be provisioned or not when/if needed //// ASSUMPTION: The decision is made based on the number of connectors of the 'appropriate' type vs. total number of connectors //// Verifies whether the object type of MVEntry in question matching the expected object type if (!IsExpectedObjectType(mventry, expectedMVObjectType)) { //// Returning 'false' since object type of MVEntry is deferent from expected/desired object type return false; } switch (expectedMVObjectType) { case MVEntryObjectType.person: { //// Declaring byte [0 to 255 range] to count number of connectors of appropriate type byte i = 0; //// Looping through each connector foreach (CSEntry csentry in mventry.ConnectedMAs[TargetMAName].Connectors) { //// Verifying whether the current connector is of 'desired' type if (IsExpectedObjectType(csentry, expectedCSObjectType)) { //// increasing the counter i++; } } //// Verifying whether the counter is greater than zero and returning the result return i.Equals(0); } default: { //// Returning false since we do not have any other MV object types defined yet return false; } } } /// <summary> /// Determies whether the user object should be renamed due to the change in the calculated distinguished name /// </summary> /// <param name="target">Management Agent in question</param> /// <param name="distinguishedName">Distinguishd name of the user object in question</param> /// <param name="expectedMVObjectType">The expected object type</param> /// <returns>The Boolean value representing whether the object should be renamed</returns> private static bool ShouldRename(ConnectedMA target, ReferenceValue distinguishedName, MVEntryObjectType expectedMVObjectType) { switch (expectedMVObjectType) { case MVEntryObjectType.person: { //// Getting collection of 'user' CSEntry objects CSEntry[] entries = GetCSEntryObjects(target, CSEntryObjectType.user); //// Verifying whether the collection contains more than one user if (!entries.Length.Equals(1)) { //// ATTENTION: Adjust business logic of this method should target MA have more than one desirable connectors of 'user' type //// Throwing an exception if collection contains more than one user throw new UnexpectedDataException("This provisioning code cannot support multiple connectors scenario(s)"); } //// Verifying whether the newly calculated distinguished name equals existing distinguished name and returning result return !entries[0].DN.Equals(distinguishedName); } default: { throw new UnexpectedDataException("This provisioning code cannot support object type " + Enum.GetName(typeof(MVEntryObjectType), expectedMVObjectType)); } } } /// <summary> /// Determines whether the provided object is of an expected object tpye /// </summary> /// <param name="mventry">The MVEntry object in question</param> /// <param name="expectedObjectType">The expected object type</param> /// <returns>Returns true when provided object is of expected object type, otherwose returns false</returns> private static bool IsExpectedObjectType(MVEntry mventry, MVEntryObjectType expectedObjectType) { //// Verifying that provided MVEntry object is an 'expected' object type return mventry.ObjectType.Equals(Enum.GetName(typeof(MVEntryObjectType), expectedObjectType), StringComparison.OrdinalIgnoreCase); } /// <summary> /// Determines whether the provided object is of an expected object tpye /// </summary> /// <param name="csentry">The CSEntry object in question</param> /// <param name="expectedObjectType">The expected object type</param> /// <returns>Returns true when provided object is of expected object type, otherwose returns false</returns> private static bool IsExpectedObjectType(CSEntry csentry, CSEntryObjectType expectedObjectType) { //// Verifying that provided CSEntry object is an 'expected' object type return csentry.ObjectType.Equals(Enum.GetName(typeof(CSEntryObjectType), expectedObjectType), StringComparison.OrdinalIgnoreCase); } /// <summary> /// Disjoins 'organizational Unit' object types from the 'person' object to avoid future accidental de-provisioning of the parent organizational unit /// </summary> /// <param name="mventry">The MVEntry object in question</param> private static void DisjoinOrganizationalUnits(MVEntry mventry) { if (!IsExpectedObjectType(mventry, MVEntryObjectType.person)) { //// Object type is not expected exting from the method return; } //// Looping through each member of joined CSEntries of 'organizational unit' type foreach (CSEntry csentry in GetCSEntryObjects(mventry.ConnectedMAs[TargetMAName], CSEntryObjectType.organizationalUnit)) { //// ATTENTION! This method will trigger deprovisioning of the 'organizationalUnit' object type. //// ATTENTION! Ensure that target management agent deprovsioning rule is configured with option 'Mark them disconnectors' //// Deprovisioning currently joined CSEntry memeber csentry.Deprovision(); } } /// <summary> /// Gets the 'expected' CSEntry objects connected to the targer management agent /// </summary> /// <param name="target">Target management agent in question</param> /// <param name="expectedObjectType">The expected object type</param> /// <returns>Collection of the CSEntry objects of 'user' type</returns> private static CSEntry[] GetCSEntryObjects(ConnectedMA target, CSEntryObjectType expectedObjectType) { //// Creating list of the CSEntry enties List<CSEntry> entries = new List<CSEntry>(); //// Looping though each entry in the list foreach (CSEntry csentry in target.Connectors) { //// Verifiying whether the current CSEntry object is the 'expected' object type if (IsExpectedObjectType(csentry, expectedObjectType)) { //// Adding CSEntry object to the list entries.Add(csentry); } } //// Transforming the List into the Array and returning the value return entries.ToArray(); } /// <summary> /// Calculate user's distinguishedName based on the set of available attributes /// </summary> /// <param name="mventry">MVEntry in question</param> /// <param name="expectedObjectType">The expected object type</param> /// <returns>Fully qualified distinguished name of the user object</returns> private static ReferenceValue GetDistinguishedName(MVEntry mventry, MVEntryObjectType expectedObjectType) { switch (expectedObjectType) { case MVEntryObjectType.person: { //// Creating Common Name string commonName = GetCommonName(mventry); //// Creating container portion of the distinguished name string container = GetContainer(mventry); //// Concatenating the distinguished name and returning the value return mventry.ConnectedMAs[TargetMAName].EscapeDNComponent(commonName).Concat(container); } default: { throw new UnexpectedDataException("Cannot process object type " + Enum.GetName(typeof(MVEntryObjectType), expectedObjectType)); } } } /// <summary> /// Creates the CN (Common Name) attribute /// </summary> /// <param name="mventry">The MVEntry object in question</param> /// <returns>Common Name</returns> private static string GetCommonName(MVEntry mventry) { //// ASSUMPTION: All attributes used in method MUST BE PRE-VERIFIED for existence of the value prior to being passed to this method. //// ATTENTION: If this method modified you must extend 'CanProvision' method //// ATTENTION: Create/Adjust CN concatenation rules here return string.Format(CultureInfo.InvariantCulture, "CN={0} {1}", mventry["givenName"].StringValue, mventry["sn"].StringValue); } /// <summary> /// Creates container portion of the distinguished name of the object /// </summary> /// <param name="mventry">MVEntry object in question</param> /// <returns>The container value of the object</returns> private static string GetContainer(MVEntry mventry) { //// ASSUMPTION: All attributes used in method MUST BE PRE-VERIFIED for existence of the value prior to being passed to this method. //// ATTENTION: If this method modified you must extend 'CanProvision' method //// ATTENTION: Create/Adjust DN concatenation rules here //// Variable containing immutable part of the target domain string domainName = string.Format(CultureInfo.InvariantCulture, @"OU=People,DC=contoso,DC=com"); //// Creating container portion of the distinguished name and returning the value return string.Format(CultureInfo.InvariantCulture, "OU={0},OU={1},{2}", mventry["title"].StringValue, mventry["department"].StringValue, domainName); } /// <summary> /// Creates organizational unit object in the target management agent /// </summary> /// <param name="target">the target management agent in question</param> /// <param name="distinguishedName">The distinguished name of the organizational unit</param> private void CreateParentOrganizationalUnitObject(ConnectedMA target, ReferenceValue distinguishedName) { //// Getting parent of the current distinguished name //// Assumption is that this method was triggered by "Missing Parent" exception and therefore the parent value will create missing parent object required for provisioning of the failed object ReferenceValue currentDistingusghedName = distinguishedName.Parent(); //// Creating new connector with the 'parent' distinguished name CSEntry csentry = target.Connectors.StartNewConnector(Enum.GetName(typeof(CSEntryObjectType), CSEntryObjectType.organizationalUnit)); try { //// Setting current distinguished name as a distinguished name for the new object csentry.DN = currentDistingusghedName; //// Committing connector to the 'connector space' of the target management agent csentry.CommitNewConnector(); //// If code reached this point the connector was successfully committed this.OnSuccessfullyCommittedObject(csentry.DN); } catch (MissingParentObjectException) { //// If MissingParentObjectException caught the parent OU object is not present //// Calling 'OnMissingParentObject' method and //// sending distinguished name of the object to be added to the list of failed objects this.OnMissingParentObject(currentDistingusghedName); //// TODO: Introduce 'fail-safe' counter to prevent possible infinite loop //// Re-calling this method recursively to create missing parent object this.CreateParentOrganizationalUnitObject(target, currentDistingusghedName); } } /// <summary> /// Creates user object in the target management agent /// </summary> /// <param name="target">The target management agent in question</param> /// <param name="distinguishedName">The distinguished name of the object</param> private void CreateUserObject(ConnectedMA target, ReferenceValue distinguishedName) { //// Creating new CSEntry of 'user' type in the tergat management agent CSEntry csentry = target.Connectors.StartNewConnector(Enum.GetName(typeof(CSEntryObjectType), CSEntryObjectType.user)); //// Assigning the distinguished name to the newly created object csentry.DN = distinguishedName; //// Committing connector to the 'connector space' of the target management agent csentry.CommitNewConnector(); //// If code reached this point the connector was successfully committed //// Calling OnSuccessfullyCommittedObject to remove current object from the list of failed objects this.OnSuccessfullyCommittedObject(csentry.DN); } /// <summary> /// The sequence of provisioning actions /// </summary> /// <param name="mventry">MVEntry object in question</param> private void ExecuteProvisioning(MVEntry mventry) { MVEntryObjectType objectType = (MVEntryObjectType)Enum.Parse(typeof(MVEntryObjectType), mventry.ObjectType, true); switch (objectType) { case MVEntryObjectType.person: { //// Determine whether we should provision "person" object as "user" object in AD //// This determination is made based on the number of connectors of the 'user' type in the target MA bool shouldProvisionUser = ShouldProvision(mventry, MVEntryObjectType.person, CSEntryObjectType.user); //// Determine whether we can or cannot provision the "person" object //// This determination is made based on the existence of necessary attributes bool canProvisionUser = CanProvision(mventry, MVEntryObjectType.person); //// Cannot provision object due to the lack of necessary attributes if (!canProvisionUser) { return; } //// (re)Calculating the distinguishedName of the 'person' object ReferenceValue distinguishedName = GetDistinguishedName(mventry, MVEntryObjectType.person); //// Declaring variable to store value representing whether user object should be renamed bool shouldRename = false; //// Creating the management agent object representing provisioning target ConnectedMA target = mventry.ConnectedMAs[TargetMAName]; //// If we should not provision a new user, should we rename/move a user? if (!shouldProvisionUser) { shouldRename = ShouldRename(target, distinguishedName, MVEntryObjectType.person); } try { //// When we should provision and can provision user if (shouldProvisionUser && canProvisionUser) { //// Provision new user object this.CreateUserObject(target, distinguishedName); } //// When we should rename/move a user if (shouldRename) { //// Renaming/Moving a user object this.RenameUserObject(target, distinguishedName); } } catch (MissingParentObjectException) { //// The MissingParentObjectException was caugh - the parent ou is missing //// Calling OnMissingParentObject to update list of failed objects this.OnMissingParentObject(distinguishedName); //// Calling the CreateParentOrganizationalUnitObject method to create parent OU this.CreateParentOrganizationalUnitObject(target, distinguishedName); } //// When code reached this point we've collected all failed objects into the list //// Loop through the list while (this.failedObjects.Count != 0) { //// Call this method recursivly to create all missing object this.ExecuteProvisioning(mventry); } break; } default: { //// Exiting if object type is NOT "person" return; } } } /// <summary> /// This method will add provided object DN into the list of failed objects /// </summary> /// <param name="failedObject">The distinguished name of the failed object</param> private void OnMissingParentObject(ReferenceValue failedObject) { if (!this.failedObjects.Contains(failedObject)) { this.failedObjects.Add(failedObject); } } /// <summary> /// This method will remove provided DN from the list of failed objects on CommitedObject event /// </summary> /// <param name="succeededObject">Object to be removed from the fault list</param> private void OnSuccessfullyCommittedObject(ReferenceValue succeededObject) { //// Verifying whether the 'failedObjects' list contains newly committed object if (this.failedObjects.Contains(succeededObject)) { //// Removing newly committed object from the failed objects list this.failedObjects.Remove(succeededObject); } } /// <summary> /// Renames the user object in the target management agent /// </summary> /// <param name="target">The target management agent in question</param> /// <param name="distinguishedName">The new distinguished name of the object</param> private void RenameUserObject(ConnectedMA target, ReferenceValue distinguishedName) { //// TODO: Adjust this method if more than one desired connector in the target management agent is required //// Getting first CSEntry which is connected to the target management agent CSEntry csentry = target.Connectors.ByIndex[0]; //// Setting new distinguished name to the object csentry.DN = distinguishedName; //// If code reached this point the connector was successfully committed this.OnSuccessfullyCommittedObject(csentry.DN); } } }

History

Please send comments/suggestions to Dmitry Kazantsev

License

This article, along with any associated source code and files, is licensed under The Common Development and Distribution License (CDDL)

——————————

Cheers,

Jorge

———————————————————————————————

* This posting is provided "AS IS" with no warranties and confers no rights!

* Always evaluate/test yourself before using/implementing this!

* DISCLAIMER: https://jorgequestforknowledge.wordpress.com/disclaimer/

———————————————————————————————

############### Jorge’s Quest For Knowledge #############

######### http://JorgeQuestForKnowledge.wordpress.com/ ########

———————————————————————————————

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

 
%d bloggers like this: