I work for an organization which must
comply with certain industry standards and business policies that affect the
governance of the management and maintenance of IT operations. As a result, the following become hot topics:
•Data retention
•Email retention
•Identity retention
•Complex account name requirements
Today, I will focus strictly on
identity retention and account name requirements. Although I will be demonstrating within the
confines of my organization's requirements, I am sure that one could apply or
modify the solution to meet their needs.
REQUIREMENTS: An identity must be
retained indefinitely given the various lifecycle changes of that
identity. This could include, but is not
limited to, contractors, contingent workers, permanent employees, vendor,
generic, and service accounts. Scenarios
might include contractor to perm, perm to contractor, partnered organization
identities and federation, termination and rehire, and the like.
We also must keep our provisioning and
deprovisioning rules simple, complex configurations at a minimum, and our
existing live data clean for daily operations.
Finally, we define that an account name will
be the sum of the first initial, middle initial, and last name, whose sum is
only up to eight characters (due to legacy system compliance within the
organization), and, when a middle name is not present, is substituted with an
"x". When a potential account
name conflicts with an existing identity, it is to be appended with a number,
starting with one, and increasing as required, while still meeting the eight
character maximum limitation.
ASSUMPTIONS: For the sake of this
solution, we will assume the following constants:
•All handling of when an employee is
provisioned or marked for deprovisioning, as well as transference from employee
to contractor, or vice versa, is handled by a combination of object state
handling in the provisioning code and advanced join/projection rules.
•If an account is deleted from the
FIM portal or Active Directory, then the object deletion rules state to remove
all other associated connectors, effectively deleting the metaverse object.
•If an account has an account name
and exists in the metaverse, it is to be projected into a SQL database for
retention purposes.
•The SQL database (or table therein)
is the retention store for all valid user/person objects, with any and all
pertinent attribute data associated with those objects.
•If an account is marked as a
potential conflict, it is deemed such in the description field of the object,
and, by means of workflow handling and notifications, handled manually by
personnel.
SOLUTION: The following solution
codes what is required to meet the above stated requirements. As objects will delete from within FIM, it is
necessary to move away from the Utils.FindMVEntries() function as a means to
check for an existing account. Instead,
we move to check accounts against the SQL table, which maintains all identity
data for retentive purposes.
Imports System
Imports Microsoft.MetadirectoryServices
Imports System.Data
Imports System.Data.SqlClient
Public Class MVProvision
Implements IMVSynchronization
Private Const MAX_ACCOUNT_PROVISION_TRIES As Integer = 1000
Private Const MAX_ACCOUNT_NAME_LENGTH As Integer = 8
Public Sub Provision( _
ByVal mventry As MVEntry) _
Implements IMVSynchronization.Provision
Dim csentry As CSEntry
Dim lastname As String
Dim displayName As String
Dim accountName As String
Dim accountXName As String
Dim conflictFound As String = Nothing
Dim provisionAD As Boolean = True
If mventry.ObjectType.Equals("person") Then
'----------------------------------------------------------------
'User Provisioning
'----------------------------------------------------------------
If provisionAD Then
'First, if the accountname isn't populated we have to calculate it.
If mventry.Item("accountName").IsPresent Then
accountName = mventry.Item("accountName").Value
displayName = mventry.Item("displayName").Value
Else
'Edit out any spaces and special characters, then concatenate to 6 characters or less
If mventry.Item("lastName").IsPresent Then
lastname = mventry.Item("lastName").Value
Else
lastname = ""
End If
lastname = lastname.Replace(" ", "") 'Remove spaces
lastname = lastname.Replace("'", "") 'Remove '
lastname = lastname.Replace("-", "") 'Remove -
lastname = lastname.Replace("_", "") 'Remove _
lastname = lastname.Replace(".", "") 'Remove .
lastname = lastname.Replace("&", "") 'Remove &
lastname = lastname.Replace("$", "") 'Remove $
lastname = lastname.Replace("@", "") 'Remove @
accountXName = Me._buildXAccountName(mventry.Item("firstName").Value, lastname)
'Check for possible conflict with "X" identity
Dim conflictFoundWithX As Boolean = Me._checkAccountWithXExists(accountXName, mventry.Item("firstName").Value, mventry.Item("lastName").Value)
If conflictFoundWithX Then
conflictFound = "Possible Username Conflict with " + accountXName
End If
'Loop until a valid account is found or you reach the max number of tries.
For i As Integer = 0 To MAX_ACCOUNT_PROVISION_TRIES
'Build the Account Name
accountName = Me._buildNextAccountName(firstinitial, middleinitial, lastname, i)
'Validate that the account is unique
If Me._checkIsAccountUnique(accountName) Then
'Success! This should be a unique account unless somebody uses it right now before we do
'Simply exit the loop and the unique account name will persist
Exit For
Else
'Identify that there may be a potential conflict
Dim conflictFoundWithAccountName As Boolean = Me._checkIsAccountConflicting(accountName, mventry.Item("firstName").Value, mventry.Item("lastName").Value)
If conflictFoundWithAccountName Then
conflictFound = "Possible Username Conflict with " + accountName
End If
End If
If i >= MAX_ACCOUNT_PROVISION_TRIES Then
'Tried to many times.
Throw New ApplicationException("Error: Unable to find a unique account name. The maximum number of tries has been reached.")
End If
Next
If Not conflictFound = Nothing Then
csentry.Item("Description").Value = conflictFound
End If
End If
successful = True
Exit Sub
End If
End If
End Sub
#Region "Helper Routines"
Private Function _buildNextAccountName(ByVal FirstName As String, ByVal MiddleName As String, ByVal LastName As String, Iteration As Integer) As String
Dim rtn As String
'Handle MiddleName in case we need to insert "X"
If MiddleName IsNot Nothing Then
If MiddleName.Substring(0, 1) = " " Then
MiddleName = "x"
Else
MiddleName = MiddleName.Substring(0, 1).ToLower
End If
Else
MiddleName = "x"
End If
'Build base user name
Dim baseUserName As String = FirstName.Substring(0, 1).ToLower() + MiddleName.Substring(0, 1).ToLower() + LastName.ToLower()
'Username must be less the MAX_ACCOUNT_NAME_LENGTH number of charachters
If baseUserName.Length > MAX_ACCOUNT_NAME_LENGTH Then
'Trim it down to MAX_ACCOUNT_NAME_LENGTH
baseUserName = baseUserName.Substring(0, MAX_ACCOUNT_NAME_LENGTH)
End If
If Iteration = 0 Then
'This is the first attempt
rtn = baseUserName
Else
'Calculate how to shift the chars over.
Dim shiftOffset As Integer = Math.Max(0, baseUserName.Length + Iteration.ToString().Length - MAX_ACCOUNT_NAME_LENGTH)
'Append a number to the end of the username
rtn = baseUserName.Substring(0, baseUserName.Length - shiftOffset) + Iteration.ToString()
End If
Return rtn
End Function
Private Function _buildXAccountName(ByVal FirstName As String, ByVal LastName As String) As String
Dim rtn As String
Dim XUserName As String = FirstName.Substring(0, 1).ToLower() + "x" + LastName.ToLower()
'Username must be less the MAX_ACCOUNT_NAME_LENGTH number of charachters
If XUserName.Length > MAX_ACCOUNT_NAME_LENGTH Then
'Trim it down to MAX_ACCOUNT_NAME_LENGTH
XUserName = XUserName.Substring(0, MAX_ACCOUNT_NAME_LENGTH)
End If
'Return username with "X" as the middle initial
rtn = XUserName
Return rtn
End Function
Private Function _checkAccountWithXExists(ByVal AccountXName As String, ByVal FirstName As String, ByVal LastName As String) As Boolean
'Setup the return object
Dim rtn As Boolean = False
Dim dt As New DataTable
'Set up the sql command string
Dim strCommand As String = "SELECT TOP 1 firstName,lastName FROM TABLENAME WHERE userName = '" + AccountXName + "';"
'Get the connection string
Dim ConnString As String = "Data Source=SERVERNAME\INSTANCE;Database=DATABASE;Trusted_Connection=yes"
Try
'Setup the connection
Using conn As New System.Data.SqlClient.SqlConnection(ConnString)
Using cmd As New System.Data.SqlClient.SqlCommand(strCommand, conn)
Try
'Open Connection
conn.Open()
cmd.CommandTimeout = 0
Dim da As New SqlDataAdapter(cmd)
'Execute the command
da.Fill(dt)
Catch ex As Exception
Throw ex
Finally
'Close connection
conn.Close()
End Try
End Using
End Using
Catch ex As Exception
'Error occured
Throw ex
End Try
If dt IsNot Nothing AndAlso dt.Rows.Count > 0 Then
'Possible conflict found, check first and last name to see if it indeed could be a conflict.
If dt.Rows(0)("lastName") IsNot Nothing AndAlso Not IsDBNull(dt.Rows(0)("lastName")) _
AndAlso dt.Rows(0)("lastName").ToString().ToLower = LastName.ToLower() Then
If dt.Rows(0)("firstName") IsNot Nothing AndAlso Not IsDBNull(dt.Rows(0)("firstName")) _
AndAlso dt.Rows(0)("firstName").ToString().ToLower = FirstName.ToLower() Then
'If first and last name exists and are the same as the target identity, then this is possibly a conflicting account.
rtn = True
End If
Else
'If things did not check out with first and last name, then there is not a conflict.
rtn = False
End If
Else
'No conflicting previous account with an "X" found.
rtn = False
End If
Return rtn
End Function
Private Function _checkIsAccountUnique(ByVal AccountName As String) As Boolean
'Setup the return object
Dim rtn As Boolean = False
Dim dt As New DataTable
'Set up the sql command string
Dim strCommand As String = "SELECT TOP 1 firstName,lastName FROM TABLENAME WHERE userName = '" + AccountName + "';"
'Get the connection string
Dim ConnString As String = "Data Source=SERVERNAME\INSTANCE;Database=DATABASE;Trusted_Connection=yes"
Try
'Setup the connection
Using conn As New System.Data.SqlClient.SqlConnection(ConnString)
Using cmd As New System.Data.SqlClient.SqlCommand(strCommand, conn)
Try
'Open Connection
conn.Open()
cmd.CommandTimeout = 0
Dim da As New SqlDataAdapter(cmd)
'Execute the command
da.Fill(dt)
Catch ex As Exception
Throw ex
Finally
'Close connection
conn.Close()
End Try
End Using
End Using
Catch ex As Exception
'Error occured
Throw ex
End Try
If dt IsNot Nothing AndAlso dt.Rows.Count > 0 Then
'Record came back --Note: This check is all you need.
'The Account Name is already used
rtn = False
Else
'As of now the account should be unique.
rtn = True
End If
Return rtn
End Function
''' <summary>
''' Check the FULLACCT table in the database to see if an account is conflicting.
''' </summary>
''' <param name="AccountName">The newly built account name</param>
''' <returns>Returns True if it is able to find the conflicting account name in the database.</returns>
''' <remarks></remarks>
Private Function _checkIsAccountConflicting(ByVal AccountName As String, ByVal FirstName As String, ByVal LastName As String) As Boolean
'Setup the return object
Dim rtn As Boolean = False
Dim dt As New DataTable
'Set up the sql command string
Dim strCommand As String = "SELECT TOP 1 firstName,lastName FROM TABLENAME WHERE userName = '" + AccountName + "';"
'Get the connection string
Dim ConnString As String = "Data Source=SERVERNAME\INSTANCE;Database=DATABASE;Trusted_Connection=yes"
Try
'Setup the connection
Using conn As New System.Data.SqlClient.SqlConnection(ConnString)
Using cmd As New System.Data.SqlClient.SqlCommand(strCommand, conn)
Try
'Open Connection
conn.Open()
cmd.CommandTimeout = 0
Dim da As New SqlDataAdapter(cmd)
'Execute the command
da.Fill(dt)
Catch ex As Exception
Throw ex
Finally
'Close connection
conn.Close()
End Try
End Using
End Using
Catch ex As Exception
'Error occured
Throw ex
End Try
If dt IsNot Nothing AndAlso dt.Rows.Count > 0 Then
'Possible conflict found, check first and last name to see if it indeed could be a conflict.
If dt.Rows(0)("lastName") IsNot Nothing AndAlso Not IsDBNull(dt.Rows(0)("lastName")) _
AndAlso dt.Rows(0)("lastName").ToString().ToLower = LastName.ToLower() Then
If dt.Rows(0)("firstName") IsNot Nothing AndAlso Not IsDBNull(dt.Rows(0)("firstName")) _
AndAlso dt.Rows(0)("firstName").ToString().ToLower = FirstName.ToLower() Then
'If first and last name exists and are the same as the target identity, then this is possibly a conflicting account.
rtn = True
End If
Else
'If things did not check out with first and last name, then there is not a conflict.
rtn = False
End If
Else
'No conflicting previous account with an "X" found.
rtn = False
End If
Return rtn
End Function
#End Region
End Class
I hope this helps. If there are any questions, feel free to ask!
-jose the admin