使用 ILM 2007/MIIS 2003 实现动态组织单元配置
ILM 2007 (MIIS2003) 配置代码设计,用于为基于 LDAP 的管理代理动态生成 OU。
引言
此项目演示了系统管理员如何使用 MIIS 2003/ILM 2007 的“脚本”配置在任何 LDAP 目录中动态创建组织单元。
背景
系统管理员经常面临在公司 LDAP 目录中创建 OU 结构的任务,例如 Active Directory、ADAM/ADLDS、OpenLDAP 等。在组织中,管理员被要求将用户帐户对象放置在与用户的部门、职称或任何其他基于用户属性动态计算的容器对应的 OU 中,他/她必须知道(因此需要硬编码)目标容器/组织单元的值在相关的 LDAP 连接目录中。
MIIS 2003/ILM 2007 的开发者参考资料中包含大量将用户帐户放置在基于 OU 名称的预定义 OU 中的示例。 如果父 OU 不可用,则管理员需要手动创建一个组织单元对象。 同时,如果组织扩展了部门列表(因此扩展了相应 OU 的列表),则必须增强配置代码以包含新值(路径)和针对新添加的目标 OU 的配置/取消配置业务逻辑。
为了避免这种为企业组织结构的每次调整而重新编译配置代码的做法,管理员可以实现一种机制,根据 Metaverse 中用户对象的属性值动态创建父组织单元。
此代码示例还为将来取消配置用户帐户提供了一条清晰的路径。 为了说明基于“用户”对象类型配置周期的 OU 动态配置的挑战,我们需要了解 *第一个* 用户帐户遇到父 OU 缺失情况的初始配置逻辑。 该代码将“检测”到父 OU 缺失,并将在目标管理代理中生成一个“organizationalUnit
”类型的 CSEntry
对象。 因此,该组织单元对象将变为(并保持)连接到用户(个人)MVEntry
对象。 任何其他用户对象随后对先前动态生成的 OU 进行的任何配置尝试都将成功。 但是,当此动态生成的 OU 中的“第一个”用户准备好取消配置时,可能会出现问题。 由于 OU 对象仍然连接到该用户对象,因此取消配置例程可能会在取消配置用户对象的同时取消配置组织单元对象,这将导致所有其他配置到同一 OU 的用户没有“父对象”。 为了避免这种不必要的状况,提供的代码示例会在同步引擎的下一个同步周期中将“organizationalUnit
”对象与“user”对象断开连接。
重要的是要确保您的配置未设置为将“organizationalUnit
”类型的断开连接器保留为“正常断开连接器”。
强烈建议管理员在实施此动态 OU 配置例程时审查所有类型的对象的取消配置逻辑。
祝您编码愉快!
Using the Code
要使用此代码,您需要下载源代码并将其编译为您的“配置”DLL。 此代码实现了 Microsoft.MetadirectoryServices.IMVSynchronization
接口。 大部分业务逻辑都在 Provision(MVEntry)
方法中实现。
//-----------------------------------------------------------------------
// <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);
}
}
}
请发送评论/建议