This is a PowerShell version 2.0 script to check proposed values for user "pre-Windows 2000 logon" names in bulk. This is the value of the sAMAccountName attribute, sometimes referred to as the NT name of the user. Sometimes an organization decides to change their standard for naming users (or any class of objects). Note, this script only considers modifying the sAMAccountName, not the Relative Distinguished Name (the common name of users), which would require a rename.

If the changes are to made in bulk using a script, several issues must be addressed. This script is designed to check the new names and consider the following:

  1. sAMAccountNames must be unique in the domain. This uniqueness is not just among user objects, but all classes of objects that have the sAMAccountName attribute, such as groups and computers.
  2. The value (for users) must be no more than 20 characters in length.
  3. The attributes of the user upon which the new value will be based, such as the first name (givenName) and last name (sn), must have values (not be missing).
  4. The following characters are not allowed in sAMAccountNames: " [ ] : ; | = + * ? < > / \ ,
  5. The new sAMAccountName value should not have any leading spaces.

The last point is to avoid having a sAMAccountName that begins with a space character. This is allowed, but makes the object very difficult to find.

This script works best if you can create a function that will return the new value for sAMAccountName for each user. This function will have input parameters for any attribute values of the user object required to determine the new value. In the script here, the new sAMAccountName is based on the values assigned to the givenName and sn attributes of the user. In particular, the new sAMAccountName will be the first letter of the givenName, followed by a period (the "." character), followed by the last name. The function Get-Name1 is used for this purpose. As an alternative, another function, Get-Name2, is provided which constructs a new name from the first initial of the first name, the middle initial, and the last name.

As you can imagine, either standard could easily result in duplicates, or even names longer than 20 characters. Also, there may be users with no first and/or last name assigned in Active Directory.

This script first retrieves all existing sAMAccountName values in the domain, regardless of the class of object. The Get-ADObject cmdlet is used for this. The hash table $Names is populated with these names. A hash table is very efficient for checking uniqueness. We know the initial values in the table are unique, because Active Directory requires it. This hash table will be used to check each of the new proposed names for uniqueness.

Next the script retrieves all users in the domain, using the Get-ADUser cmdlet. Then the script considers each user in turn and performs the checks described above.

If the new sAMAccountName value passes all of these checks, then the new value is added to the hash table to prevent duplicates. The script can be used to actually update the user object with the new value. If the user object is updated, any possible error is trapped, so the problem can be logged but the script can continue.

The script documents everything in a specified log file. There can be several lines for every user considered by the script. If updating the user raised an error, the corresponding line in the log file begins with the string "## Error:". This allows you to search for errors in the log file even if it is very large.

The script is designed to be run first without updating users, so you can check the log file for problems. You may be able to fix the problems. Or, you may need to adjust the Get-Name function to prevent the problems.

When a duplicate name is detected, the script adds the distinguished name of the user to an array. After all users have been processed, the script loops through the array of users that had conflicts and checks again to see if the new name is still a duplicate. At the same time, the script also considers 3 variations of the new name, with the digits "2", "3", and "4" appended. If any of these is available, it is used to update the user sAMAccountName. You might want to adjust this.

When the script completes, it appends the following totals to the log file:

Total Label in Log File
New Name Truncated ## Truncated:
Invalid Characters Removed ## Invalid Characters Removed:
Users Changed Renamed:
Renamed (Second Attempt):
Users Could be Changed Can be Renamed:
Users no Change Needed (Skipped) ## OK:
Users with Missing Values (Skipped) ## Missing Values:
New Name not Unique (Skipped) ## Not Unique:
## Not Unique (Second Attempt):
Number of Errors Updating ## Error:
Total Number of Users Processed -
More Than 10 Errors ## More Than 10 Errors

You can search the log file for the labels in the table to find details about the problems.

Suggested steps to use the script:

  1. Update the Get-Name function (Get-Name1 or Get-Name2 or your own function) to meet your needs.
  2. Modify the script to use your function, if it is not Get-Name1. The function is called in two places.
  3. Modify the Get-ADUser command (two places), if necessary, to retrieve the attributes needed by your Get-Name function.
  4. Add -SearchBase to the Get-ADUser cmdlets (two places) if you want to only consider users in an organizational unit.
  5. Run the script with $Update = $False.
  6. Check the log file for problems. The totals at the bottom of the log will show the number of problems. You can search the log for the labels in the table above for details.
  7. Modify user values (such as first and last names) or your Get-Name function if necessary to fix problems (if possible). Otherwise, the script will skip problems and you can deal with them manually.
  8. Rename the log file each time you run the script, or it will append to the same file.
  9. Run the script with $Update = $True to actually update the users.

 

PowerShell
Edit|Remove
# CheckNTNames.ps1 
# PowerShell V2 script to check proposed pre-Windows 2000 names. 
# To be used when the standard for sAMAccountNames is changed, and 
# users are to be updated in bulk. This script ensures that the 
# new values conform to the following: 
# 1. The sAMAccountName values must be unique in the domain. 
# 2. The value can be no more than 20 characters long. 
# 3. The attributes on which the new value will be based must not be missing. 
# 4. The following characters are not allowed: " [ ] : ; | = + * ? < > / \ , 
# 5. Values must not include a leading blank. 
# Author: Richard L. Mueller 
# Version 1.0 - February 20, 2015 
 
Write-Host "Please Standby..." 
 
# Load the PowerShell Active Directory Module. 
Import-Module ActiveDirectory 
 
# Flag to indicate whether objects will be updated. 
# Change $Update to $True to have sAMAccountName values updated. 
$Update = $False 
 
# Setup the log file. 
$LogFile = "CheckNTNames.log" 
Add-Content -Path $LogFile -Value "------------------------------------------------" ` 
    -ErrorAction Stop 
Add-Content -Path $LogFile -Value "CheckNTNames.ps1 Version 1.0 (February 20, 2015)" 
Add-Content -Path $LogFile -Value $("Started: " + (Get-Date).ToString()) 
Add-Content -Path $LogFile -Value "Update flag: $Update" 
Add-Content -Path $LogFile -Value "Log file: $LogFile" 
Add-Content -Path $LogFile -Value "------------------------------------------------" 
 
# Initialize counters. 
$Changed = 0 
$Missing = 0 
$NotUnique = 0 
$Script:Long = 0 
$Script:Invalid = 0 
$OK = 0 
$Errors = 0 
$Total = 0 
 
# Flag to abort if there are too many errors attempting to update users. 
$Abort = $False 
 
# Function to remove invalid characters. 
Function RemoveInvalid($NewNTName$DN) 
{ 
    [regex]$Reg = "(`"|:|;|=|<|>|/|,|\?|\[|\]|\||\+|\*|\\)" 
    If ($Reg.Matches($NewNTName).Count -eq 0) 
    { 
        If ($NewNTName.Length -gt 0) {$NewNTName = $NewNTName.Trim()} 
        Return $NewNTName 
    } 
    Else 
    { 
        Add-Content -Path $LogFile -Value $("## Invalid Characters Removed: "` 
            + "Name $NewNTName") 
        # Remove invalid characters and trim leading and trailing blanks. 
        $NewNTName = $NewNTName.Replace("`"", "").Replace("[", "").Replace("]", "") 
        $NewNTName = $NewNTName.Replace(":""").Replace(";""").Replace("|""") 
        $NewNTName = $NewNTName.Replace("=""").Replace("+""").Replace("*""") 
        $NewNTName = $NewNTName.Replace("?""").Replace("<""").Replace(">""") 
        $NewNTName = $NewNTName.Replace("\", "").Replace("/", "") 
        If ($NewNTName.Length -gt 0) {$NewNTName = $NewNTName.Trim()} 
        Add-Content -Path $LogFile -Value $("    Revised Name: $NewNTName") 
        Add-Content -Path $LogFile -Value $("    DN: $DN") 
        $Script:Invalid = $Script:Invalid + 1 
        Return $NewNTName 
    } 
} 
 
# Function to truncate names longer than 20 characters. 
Function Truncate($NewNTName$DN) 
{ 
    If ($NewNTName.Length -le 20) {Return $NewNTName} 
    Else 
    { 
        Add-Content -Path $LogFile -Value $("## Truncated: sAMAccountName " ` 
            + $NewNTName + " is too long") 
        # Truncate to first 20 characters. 
        $NewNTName = $NewNTName.SubString(0, 20).Trim() 
        Add-Content -Path $LogFile -Value $("    Revised sAMAccountName: $NewNTName") 
        Add-Content -Path $LogFile -Value $("    DN: $DN") 
        $Script:Long = $Script:Long + 1 
        Return $NewNTName 
    } 
} 
 
# Function to determine new sAMAccountName value based on first and last names, 
# in form "f.last" where "f" is the first initial of the first name and 
# "last" is the last name. 
Function Get-Name1($FirstName$LastName$DN) 
{ 
    # Remove invalid characters from all values and trim leading and trailing spaces. 
    $FirstName = RemoveInvalid $FirstName $DN 
    $LastName = RemoveInvalid $LastName $DN 
    # Check that neither parameter is missing. 
    If (($FirstName-and ($LastName)) 
    { 
        # Determine proposed value of sAMAccountName. 
        # First letter of first name, following by ".", followed by last name. 
        # Make the value all lower case. 
        Return $FirstName.SubString(0, 1).ToLower() + "." ` 
            + $LastName.ToLower() 
    } 
    Else {Return "#=Problem"} 
} 
 
# Function to determine new sAMAccountName value based on first and last names and 
# the middle initial, in form "fmlast", where "f" is the first initial of the first name, 
# "m" is the middle initial, and "last" is the last name. If there is a first and last 
# name, but not a middle initial, the sAMAccountName will be "flast". 
Function Get-Name2($FirstName$Middle$LastName$DN) 
{ 
    # Remove invalid characters from all values and trim leading and trailing spaces. 
    $FirstName = RemoveInvalid $FirstName $DN 
    $LastName = RemoveInvalid $LastName $DN 
    $Middle = RemoveInvalid $Middle $DN 
    # Check that neither first nor last name are missing. 
    # It is acceptable for the middle initial to be missing. 
    If (($FirstName-and ($LastName)) 
    { 
        If ($Middle) 
        { 
            # Determine proposed value of sAMAccountName. 
            # First letter of first name, following by the middle initial, 
            # followed by last name. 
            # Make the value all lower case. 
            Return $FirstName.SubString(0, 1).ToLower() ` 
                + $Middle.SubString(0, 1).ToLower() + $LastName.ToLower() 
        } 
        Else 
        { 
            # No middle initial. 
            # Make the value all lower case. 
            Return $FirstName.SubString(0, 1).ToLower() + $LastName.ToLower() 
        } 
    } 
    Else {Return "#=Problem"} 
} 
 
# Function to pad with blanks and right justify formatted integer values. 
Function RightJustify($Value$Size) 
{ 
    # Format integer value for readability and pad with 10 blanks on the left. 
    $Padded = "          $('{0:n0}' -f $Value)" 
    # Right justify as much as needed to accomodate largest value. 
    Return $Padded.SubString($Padded.Length - $Size$Size) 
} 
 
# Function to add counter totals to the log file. 
Function Add-Totals 
{ 
    # Maximum integer length for right justifying the output. 
    $TotalSize = $('{0:n0}' -$Total).Length 
 
    Add-Content -Path $LogFile -Value "------------------------------------------------" 
    Add-Content -Path $LogFile -Value $("Finished: " + (Get-Date).ToString()) 
    Add-Content -Path $LogFile ` 
        -Value "New Name Truncated:                  $(RightJustify $Script:Long $TotalSize)" 
    Add-Content -Path $LogFile ` 
        -Value "Invalid Characters Removed:          $(RightJustify $Script:Invalid $TotalSize)" 
    Add-Content -Path $LogFile -Value "                                    ------------" 
    If ($Update -eq $True) 
    { 
        Add-Content -Path $LogFile ` 
            -Value "Users Renamed:                       $(RightJustify $Changed $TotalSize)" 
    } 
    Else 
    { 
        Add-Content -Path $LogFile ` 
            -Value "Users Can be Renamed:                $(RightJustify $Changed $TotalSize)" 
    } 
    Add-Content -Path $LogFile ` 
        -Value "Users no Change Needed (Skipped):    $(RightJustify $OK $TotalSize)" 
    Add-Content -Path $LogFile ` 
        -Value "Users with Missing Values (Skipped): $(RightJustify $Missing $TotalSize)" 
    Add-Content -Path $LogFile ` 
        -Value "New Name not Unique (Skipped):       $(RightJustify $NotUnique $TotalSize)" 
    If ($Update -eq $True) 
    { 
        Add-Content -Path $LogFile ` 
            -Value "Number of Errors Updating:           $(RightJustify $Errors $TotalSize)" 
    } 
    Add-Content -Path $LogFile -Value "                                    ------------" 
    Add-Content -Path $LogFile ` 
        -Value "Total Number of Users Processed:     $('{0:n0}' -f $Total)" 
} 
 
# Hash table of sAMAccountNames to check for uniqueness. 
$Names = @{} 
 
# Array of objects to be checked for uniqueness a second time. 
$Dups = @() 
 
# Retrieve all objects in the domain with sAMAccountName values. 
$Objects = Get-ADObject -LDAPFilter "(sAMAccountName=*)" -Properties sAMAccountName 
 
# Populate the hash table. Key is sAMAccountName, value is distinguishedName. 
ForEach ($Object In $Objects) 
{ 
    $Names.Add($Object.sAMAccountName, $Object.distinguishedName) 
} 
 
# Retrieve all user objects in the domain that are to have new sAMAccountName values. 
# If instead you only want to consider users in a specified organizational unit, 
# add the -SearchBase parameter. 
# For example: -SearchBase "ou=Sales,ou=West,dc=MyDomain,dc=com" 
$Users = Get-ADUser -Filter * ` 
    -Properties GivenName, Initials, Surname, sAMAccountName, distinguishedName 
 
# Check each user. 
ForEach ($User In $Users) 
{ 
    $Total = $Total + 1 
    # Determine the new sAMAccountName, using Function Get-Name1. This function 
    # should remove invalid characters and trim leading or trailing spaces from values. 
    $NewName = Get-Name1 $User.GivenName $User.Surname $User.distinguishedName 
    # If you use Get-Name2, use the following instead: 
    # $NewName = Get-Name2 ` 
    #     $User.GivenName $User.Initials $User.Surname $User.distinguishedName 
    # Check for problem (missing values). 
    If ($NewName -ne "#=Problem") 
    { 
        # Truncate, if necessary, to 20 characters. 
        $NewName = Truncate $NewName $User.distinguishedName 
        # Check if user sAMAccountName already in correct format. 
        $OldName = $User.sAMAccountName 
        If ($OldName.ToLower() -ne $NewName.ToLower()) 
        { 
            # Check that new name is unique in the domain. 
            If ($Names.ContainsKey("$NewName"-eq $False) 
            { 
                # New NT name is OK. 
                # Only update if the flag is set. 
                If ($Update -eq $True) 
                { 
                    # Catch any possible errors, such as lacking permissions. 
                    Try 
                    { 
                        Set-ADUser -Identity $User.distinguishedName ` 
                            -Replace @{sAMAccountName=$NewName.ToString()} 
                        # Add the new name to the hash table. 
                        $Names.Add($NewName$User.distinguishedName) 
                        # Remove the old name from the hash table. 
                        $Names.Remove($OldName) 
                        Add-Content -Path $LogFile ` 
                            -Value $("Renamed: From $OldName to $NewName")     
                        Add-Content -Path $LogFile -Value $("    DN: " ` 
                            + $User.distinguishedName) 
                        $Changed = $Changed + 1 
                    } 
                    Catch 
                    { 
                        Add-Content -Path $LogFile -Value $("## Error: " ` 
                            + " failed to assign sAMAccountName $NewName") 
                        Add-Content -Path $LogFile ` 
                            -Value $("    DN: " + $User.distinguishedName) 
                        Add-Content -Path $LogFile -Value "    Error Message: $_" 
                        $Errors = $Errors + 1 
                        # Allow only 10 errors before aborting the script. 
                        If ($Errors -gt 10) 
                        { 
                            $Abort = $True 
                            Add-Content -Path $LogFile ` 
                                -Value "## More Than 10 Errors" 
                            Add-Content -Path $LogFile ` 
                                -Value "    Script Aborted" 
                            Write-Host "More Than 10 Errors Encountered!!" 
                            Write-Host "Script Aborted." 
                            # Break out of the ForEach loop. 
                            Break 
                        } 
                    } 
                } 
                Else 
                { 
                    # User could be updated, if update flag set. 
                    # Add the new name to the hash table. 
                    $Names.Add($NewName$User.distinguishedName) 
                    Add-Content -Path $LogFile -Value $("Can be Renamed:" ` 
                        + " From $OldName to $NewName") 
                    Add-Content -Path $LogFile -Value $("    DN: " + $User.distinguishedName) 
                    $Changed = $Changed + 1 
                } # End If $Update -eq $True. 
            } 
            Else 
            { 
                # New sAMAccountName not unique in the domain. 
                Add-Content -Path $LogFile -Value $("## Not Unique: " ` 
                    + "sAMAccountName $NewName not unique in domain") 
                Add-Content -Path $LogFile -Value $("    DN: " + $user.distinguishedName) 
                Add-Content -Path $LogFile -Value $("    Conflicts with: " ` 
                    + $Names[$NewName]) 
                # The value will be checked again later. 
                $Dups = $Dups + $User.distinguishedName 
                $NotUnique = $NotUnique + 1 
            } # End If $NewName unique. 
        } 
        Else 
        { 
            # sAMAccountName already in desired format. 
            Add-Content -Path $LogFile -Value $("## OK: " ` 
                + "sAMAccountName $NewName already in correct format") 
            Add-Content -Path $LogFile -Value $("    DN: " + $user.distinguishedName) 
            $OK = $OK + 1 
        } # End $NewName already correct. 
    } 
    Else 
    { 
        # Problem determining new sAMAccountName. 
        Add-Content -Path $LogFile -Value $("## Missing Values: Attributes missing") 
        Add-Content -Path $LogFile -Value $("    DN: " + $user.distinguishedName) 
        $Missing = $Missing + 1 
    } # End missing attributes. 
} # End ForEach $User. 
 
# Check any duplicates found to make sure they are still duplicates. 
# This check only helps if $Update is set to $True, and we are not 
# aborting due to too many errors. 
If (($Dups.Count -gt 0) -and ($Update -eq $True-and ($Abort -eq $False)) 
{ 
    Add-Content -Path $LogFile -Value "------------------------------------------------" 
    Add-Content -Path $LogFile ` 
        -Value "Duplicate values for sAMAccountName will be checked again" 
    Add-Content -Path $LogFile -Value "------------------------------------------------" 
 
    # Update hash table of sAMAccountNames to check for uniqueness. 
    $Names = @{} 
 
    # Retrieve all objects in the domain with sAMAccountName values. This now 
    # includes all new values, and does not include old values that were updated. 
    $Objects = Get-ADObject -LDAPFilter "(sAMAccountName=*)" -Properties sAMAccountName 
 
    #Populate the hash table. Key is sAMAccountName, value is distinguishedName. 
    ForEach ($Object In $Objects) 
    { 
        $Names.Add($Object.sAMAccountName, $Object.distinguishedName) 
    } 
    # Consider each user previously found to be in conflict with another object. 
    ForEach ($UserDN In $Dups) 
    { 
        # Retrieve the user attributes, as required by function Get-Name1 or Get-Name2. 
        $User = Get-ADUser -Identity $UserDN ` 
            -Properties GivenName, Initials, Surname, sAMAccountName, distinguishedName 
        # Determine the new sAMAccountName, using the function Get-Name1. 
        $NewName = Get-Name1 $User.GivenName $User.Surname $User.distinguishedName 
        # If you use Get-Name2, use the following instead: 
        # $NewName = Get-Name2 $User.GivenName $User.Initials $User.Surname ` 
        #     $User.distinguishedName 
        # Truncate, if necessary, to 20 characters. 
        $NewName = Truncate $NewName $User.distinguishedName 
        # In case of conflicts, consider variations of sAMAccountName values. 
        If ($NewName.Length -le 19) 
        { 
            $NewName2 = "$NewName`2" 
            $NewName3 = "$NewName`3" 
            $NewName4 = "$NewName`4" 
        } 
        Else 
        { 
            # The length cannot exceed 20 characters. 
            $NewName2 = $NewName.SubString(0, 19).Trim() + "2" 
            $NewName3 = $NewName.SubString(0, 19).Trim() + "3" 
            $NewName4 = $NewName.SubString(0, 19).Trim() + "4" 
        } 
        # Check that new name is unique in the domain. 
        # Other checks have already been performed. 
        # Check all variations of sAMAccountName. 
        If ($Names.ContainsKey("$NewName"-eq $False) {$NTName = $NewName} 
        Else 
        { 
            If ($Names.ContainsKey("$NewName2"-eq $False) {$NTName = $NewName2} 
            Else 
            { 
                If ($Names.ContainsKey("$NewName3"-eq $False) {$NTName = $NewName3} 
                Else 
                { 
                    If ($Names.ContainsKey("$NewName4"-eq $False) {$NTName = $NewName4} 
                    Else {$NTName = "#=Conflict"} 
                } 
            } 
        } 
        If ($NTName -ne "#=Conflict") 
        { 
            # New NT name is OK. 
            # The previous problem will either be resolved, or an error 
            # will be raised, so reduce the $NotUnique count by 1. 
            $NotUnique = $NotUnique - 1 
            # Catch any possible errors, such as lacking permissions. 
            Try 
            { 
                $OldName = $User.sAMAccountName 
                Set-ADUser -Identity $User.distinguishedName ` 
                    -Replace @{sAMAccountName=$NTName.ToString()} 
                # Add the new name to the hash table. 
                $Names.Add($NTName$User.distinguishedName) 
                # Remove old name from the hash table. 
                $Names.Remove($OldName) 
                Add-Content -Path $LogFile ` 
                    -Value $("Renamed (Second Attempt): From $OldName to $NTName")     
                Add-Content -Path $LogFile -Value $("    DN: " ` 
                    + $User.distinguishedName) 
                # The previous problem has been resolved. 
                $Changed = $Changed + 1 
            } 
            Catch 
            { 
                Add-Content -Path $LogFile -Value $("## Error: " ` 
                    + " failed to assign sAMAccountName $NTName") 
                Add-Content -Path $LogFile ` 
                    -Value $("    DN: " + $User.distinguishedName) 
                Add-Content -Path $LogFile -Value "    Error Message: $_" 
                $Errors = $Errors + 1 
                # Allow only 10 errors before aborting the script. 
                If ($Errors -gt 10) 
                { 
                    Add-Content -Path $LogFile ` 
                        -Value "## More Than 10 Errors" 
                    Add-Content -Path $LogFile ` 
                        -Value "    Script Aborted" 
                    Write-Host "More Than 10 Errors Encountered!!" 
                    Write-Host "Script Aborted." 
                    # Break out of the ForEach loop. 
                    Break 
                } 
            } 
        } 
        Else 
        { 
            # New sAMAccountName, and variations, are not unique in the domain. 
            Add-Content -Path $LogFile -Value $("## Not Unique (Second Attempt): " ` 
                + " sAMAccountName $NewName not unique in domain") 
            Add-Content -Path $LogFile -Value $("    DN: " + $User.distinguishedName) 
            Add-Content -Path $LogFile -Value "    Conflicts with users:" 
            Add-Content -Path $LogFile -Value $("    $Names[$NewName]") 
            Add-Content -Path $LogFile -Value $("    $Names[$NewName2]") 
            Add-Content -Path $LogFile -Value $("    $Names[$NewName3]") 
            Add-Content -Path $LogFile -Value $("    $Names[$NewName4]") 
        } # End attempt to assign new name. 
    } # End ForEach $UserDN to be reconsidered. 
} # End duplicate sAMAccountNames to be reconsidered. 
 
# Add totals to the log file. 
Add-Totals 
 
Write-Host "Done. See log file: $LogFile"