Monday, May 20, 2013

Record retention and account name building strategies with FIM 2010

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