Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Dynamic Organizational Unit Provisioning with ILM 2007/MIIS 2003

5.00/5 (1 vote)
5 May 2009CDDL3 min read 26.4K   83  
ILM 2007 (MIIS2003) Provisioning code design to dynamically generate OUs for LDAP-base management agents.

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.

C#
//-----------------------------------------------------------------------
// <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);
    }
}
}

Please send comments/suggestions

License

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