Skip to the content.

Summary

Local user accounts in MacOS & Windows can be renamed with a remote automation. The requirements for these operating systems differ but the workflow detailed below is generally the same between the two.

In the macOS operating system workflow, users are logged out to the desktop, the automation runs the in the background and finally after a loginwindow refresh, the user whose account was renamed can login with their old password (local password)/ new password (JumpCloud Password)

In the Windows operating system workflow, users are simply renamed with the rename-localUser cmdlet. Their login username is refreshed after a successful login/ restart.

MacOS Rename Script Updated June 26 2024 to restart the device after rename and properly determine if the CSV contains the system record.

Basic Usage

  • The PowerShell Module is required to run this script
  • Run a PowerShell script to determine what local user accounts are on each system, create a CSV with this data
  • Manually edit this CSV file to determine which local user accounts should be renamed
  • Validate this CSV file, create a new CSV file with stripped fields + upload to public location
  • Create JumpCloud commands in the administrator portal
  • Run the corresponding renameLocalUser commands on JumpCloud managed systems.

Additional Information

To begin renaming local users, I’ve created a quick automation to help gather data about systems in a JumpCloud organization. This automation will produce a CSV with a systemID, renameUser & jumpcloudUser columns which we’ll pass to the individual systems to kick off the rename scripts. ex:

systemID renameUser jumpcloudUser
609e98bd32a14f0f822e598d Farmer_103 farmer.103
60afdc5821a64317675f9f87 Farmer_367 farmer.367
60d347fab6a44816c875725c Farmer_432 farmer.432
610b255f8973970707a1e439 Farmer_238 farmer.238

JumpCloud systems run a script to download the CSV. Once downloaded, the script identifies the corresponding line containing it’s systemID and attempts to migrate the renameUser to the jumpcloudUser username. Ex:

If a JumpCloud system with ID 60d347fab6a44816c875725c runs the migration script:

  • The host will download the entire CSV
  • The script checks gets the JumpCloud systemID from a config.json file locally on the system.
  • If the systemID matches a systemID in the CSV, the script notes the renameUser:Farmer_432 username and attempts to migrate to jumpCloudUser:farmer.432
  • If the script was able to rename the user, it’ll bind the JumpCloud user to the system which will initiate an account takeover.

Create the CSV

To get all the local username data about JumpCloud systems in an organization, we can run a powershell script to get each systems local username data

# Get Data
$Data = Get-JCSystemInsights -Table User | Where-Object { ($_.Directory -match "C:\\Users*") -or ($_.Directory -match "/Users/*") -And ($_.username -ne "") } | Select-Object username, Directory, systemid
# Add column for jumpcloud username
$Data | Add-Member -Name 'jumpcloudUsername' -Value '' -MemberType NoteProperty
# export
$Data | Export-Csv -Path ./DiscoveryUsernames.csv

The code snippet above should generate a CSV similar to the following:

Username Directory SystemId jumpcloudUsername
totpuser /Users/totpuser 5e90f19ecc99d210ecb3406c  
defaultadmin /Users/defaultadmin 6010bf24471b3b196e81e888  
Farmer_103 /Users/Farmer_103 609e98bd32a14f0f822e598d  
Farmer_367 /Users/Farmer_367 60afdc5821a64317675f9f87  

Manually iterate through each row and identify the systemsIDs/ usernames that need to be migrated. Add the JumpCloud Username of the users who should be migrated and delete the remaining rows. Only one unique systemID per CSV will work for this automation. I.e. the script can not migrate two users per system. Finally run this automation to validate and create the final CSV which we’ll pass to systems before migration.

# Get CSV
$data = Import-CSV -path ./DiscoveryUsernames.CSV
$dupes = $Data.SystemID | Group-Object
$dupeExit = $false
foreach ($line in $dupes) {
    if (($line.Count) -gt 1) {
        # If multiple entries are found for a given system, throw this error
        Write-Error "Multipe entries Found for SystemID: $($line.Name)"
        $dupeExit = $true
    }
}
if ($dupeExit) {
    Write-warning "exiting..."
    break
}

$newData = @()
foreach ($line in $data) {
    # $line
    # Validate User and JumpCLoudUserName Fields
    if ( [string]::IsNullOrEmpty($($line.Username)) ) {
        Write-Error "Missing username entry for SystemID: $($line.Name)"
        break
    }
    if ( [string]::IsNullOrEmpty($($line.jumpcloudUsername)) ) {
        Write-Error "Missing JumpCloud username entry for SystemID: $($line.Name)"
        break
    }
    if ( [string]::IsNullOrEmpty($($line.systemID)) ) {
        Write-Error "Missing systemID entry $($line)"
        break
    }
    # Finally create new CSV
    $newData += [PSCustomObject]@{
        systemID      = $line.systemID
        renameUser    = $line.Username
        jumpcloudUser = $line.jumpcloudUsername
    }
}
# Finally export the "MigrateUsers.CSV file
$newData | Export-Csv ./MigrateUsers.CSV

The resulting MigrateUsers.CSV should be uploaded directly to a JumpCloud Command.

Add the scripts to JumpCloud

Create two commands in the JumpCloud console and copy the renameLocalUsers.ps1 & the reanmeLocalUsers.sh code into Windows Powershell & MacOS commands (respectively).

Replace the variables in the commands:

Windows Powershell

# Your JumpCloud Administrator API Key; required to bind user
$jumpcloudAPIKey = 'yourJumpCloudAPIKey'
### Bind user as admin or standard user ###
# Admin user: admin="true"
# Standard user: admin="false"
$admin = $true

macOS

# Your JumpCloud Administrator API Key; required to bind user
jumpcloudAPIKey='yourJumpCloudAPIKey'
### Bind user as admin or standard user ###
# Admin user: admin="true"
# Standard user: admin="false"
admin="true"

Run these commands on JumpCloud managed macOS and Windows systems. Test before mass deploying.

Scripts

Windows Powershell

################################################################################
# Update Variables Below
################################################################################

# Your JumpCloud Administrator API Key; required to bind user
$jumpcloudAPIKey = 'yourAPIKey'
### Bind user as admin or standard user ###
# Admin user: admin="true"
# Standard user: admin="false"
$admin = $true

########## Do not edit below ##########

################################################################################
# Get the JumpCloud SystemID
################################################################################

$config = get-content 'C:\Program Files\JumpCloud\Plugins\Contrib\jcagent.conf'
$regex = 'systemKey\":\"(\w+)\"'
$systemKey = [regex]::Match($config, $regex).Groups[1].Value
if ([string]::IsNullOrEmpty($systemKey)) {
    Write-Host "JumpCloud SystemID could not be verified, exiting..."
    exit 1
}
################################################################################
# Download CSV
################################################################################
$tempPath = "C:\Windows\Temp\MigrateUsers.csv"
# Download the Font to a Temp Location in C:
$CSV = Get-Content -Path $tempPath | ConvertFrom-Csv
foreach ($line in $CSV) {
    <# $line is the current item #>
    if ($line.systemID -eq $systemKey) {
        Write-Host "SystemID: $($line.systemID)"
        Write-Host "UserToRename: $($line.renameUser)"
        Write-Host "JumpCloudUsername: $($line.jumpcloudUser)"
        $UserToRename = $($line.renameUser)
        $jumpcloudUsername = $($line.jumpcloudUser)
    }
}

if (([string]::IsNullOrEmpty($UserToRename)) -or ([string]::IsNullOrEmpty($jumpcloudUsername))) {
    Write-Host "User To Rename/ JumpCloud User/ System Record not found in CSV, exiting ..."
    exit 1
}

################################################################################
# Search for matching JumpCloud UserName
################################################################################
$jcUrl = "https://console.jumpcloud.com/api/search/systemusers"
$jcHeaders = @{
    "x-api-key"    = $jumpcloudAPIKey
    "Content-Type" = "application/json"
    "Accept"       = "application/json"
}

$jcBody = @{
    'filter' = "username:eq:$jumpcloudUsername"
    "fields" = "username"
} | ConvertTo-Json
Try {
    $response = Invoke-WebRequest -Uri $jcUrl -Headers $jcHeaders -body $jcBody -Method Post -UseBasicParsing
    $Results = $Response.Content | ConvertFrom-Json
    $StatusCode = $Response.StatusCode
} Catch {
    $StatusCode = $_.Exception.Response.StatusCode.value__
}
If ($StatusCode -ne 200) {
    Write-Host "JumpCloud username: $jumpcloudUsername not matched in directory"
    exit 1
}
If ($Results.totalCount -eq 1 -and $($Results.results[0].username) -eq $jumpcloudUsername) {
    Write-Host "Validated JumpCloud Username"
    $userID = $($Results.results._id)
}

################################################################################
# Gather User Variables
################################################################################

$users = Get-LocalUser
foreach ($user in $users) {
    if ($UserToRename -eq $user.Name) {
        Write-Host "Matched $UserToRename user found"
        $selectedUser = $UserToRename
    }
}
# If selectedUser is Null, exit
if ([string]::IsNullOrEmpty($selectedUser)) {
    Write-Host "Could not find user exiting... the following users exist:"
    Write-Host "$($users.Name)"
    exit 1
} else {
    Write-Host "$selectedUser will be renamed"
}


################################################################################
# Echo Status
################################################################################
Write-Host "###############################################"
Write-Host "### $selectedUser will be renamed to $jumpcloudUsername ###"
Write-Host "### $jumpcloudUsername is a jumpcloud user with userID: $userID ###"
Write-Host "### This system is jumpcloud bound with ID: $systemKey ###"
Write-Host "###############################################"

################################################################################
# migration script begin
################################################################################
# Rename Local User
Write-Host "Rename $selectedUser username to be $jumpcloudUsername"
Rename-LocalUser -Name $selectedUser -NewName $jumpcloudUsername

# Bind User Account
$jcUrl = "https://console.jumpcloud.com/api/v2/systems/$systemKey/associations"
$jcBody = @{
    'attributes' = @{
        'sudo' = @{
            'enabled'         = $admin
            'withoutPassword' = $false
        }
    }
    'op'         = 'add'
    'type'       = 'user'
    'id'         = $userID
} | ConvertTo-Json
Try {
    $responseBind = Invoke-WebRequest -Uri $jcUrl -Headers $jcHeaders -body $jcBody -Method Post
    $Results = $responseBind.Content | ConvertFrom-Json
    $StatusCode = $responseBind.StatusCode
} Catch {
    $StatusCode = $_.Exception.responseBind.StatusCode.value__
}

macOS Script

#!/bin/bash

################################################################################
# Update Variables Below
################################################################################

# Url where system/user CSV is stored:
url="https://path/to/example.csv"
# Your JumpCloud Administrator API Key; required to bind user
jumpcloudAPIKey='yourAPIKey'
### Bind user as admin or standard user ###
# Admin user: admin="true"
# Standard user: admin="false"
admin="true"

########## Do not edit below ##########

################################################################################
# Get the JumpCloud SystemID
################################################################################
conf="$(cat /opt/jc/jcagent.conf)"
regex='\"systemKey\":\"[a-zA-Z0-9]{24}\"'
if [[ $conf =~ $regex ]]; then
    systemKey="${BASH_REMATCH[@]}"
fi
regex='[a-zA-Z0-9]{24}'
if [[ $systemKey =~ $regex ]]; then
    systemID="${BASH_REMATCH[@]}"
else
    echo "JumpCloud SystemID could not be verified, exiting..."
    exit 1
fi

################################################################################
# Download CSV
################################################################################
# Set temp location for user CSV
csv="/tmp/MigrateUsers.csv"
# Read out lines in CSV, if match between CSV && SystemID, populate vars
while IFS="," read -r CSV_systemID CSV_UserToRename CSV_JumpCloudUser
do
    sysId=$(echo $CSV_systemID | tr -d '"')
    if [[ $sysId == "$systemID" ]];then
        echo "SystemID: $CSV_systemID"
        echo "UserToRename: $CSV_UserToRename"
        # strip whitespace if exists
        UserToRename=$(echo "$CSV_UserToRename" | xargs)
        echo "JumpCloudUsername: $CSV_JumpCloudUser"
        # strip whitespace if exists
        jumpcloudUsername=$(echo "$CSV_JumpCloudUser" | xargs)
        echo ""
    fi
done < $csv

################################################################################
# Get Local Accounts on system and see if UserToRename exists
################################################################################
# Get the localAccounts on the system
localAccounts=$(dscl . list /Users UniqueID | awk '$2>500{print $1}' | grep -v super.admin | grep -v _jumpcloudserviceaccount)

for usr in ${localAccounts};
do
    if [[ $usr == $UserToRename ]]; then
        echo "Matched $UserToRename user found"
        # selectedUser is the username we'll be renaming
        selectedUser=$UserToRename
    fi
done

# If selectedUser is Null, exit
if [[ $selectedUser == "" ]]; then
    echo "Could not find user exiting... the following users exist:"
    echo "$localAccounts"
    exit 1
else
    echo "$selectedUser will be renamed"
fi
################################################################################
# Search for matching JumpCloud UserName
################################################################################
userSearch=$(
    curl -s -X POST https://console.jumpcloud.com/api/search/systemusers \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H "x-api-key: ${jumpcloudAPIKey}" \
  -d '{"filter":[{"username":"'$jumpcloudUsername'"}],"fields":["username"]}'
)
regex='[a-zA-Z0-9]{24}'
if [[ $userSearch =~ $regex ]]; then
    userID="${BASH_REMATCH[@]}"
else
    exit 1
fi

# Validate username
if [[ $userSearch == "" ]]; then
    echo "JumpCloud username: $jumpcloudUsername not matched in directory"
    exit 1
else
    echo "validated JumpCloud username"
fi

################################################################################
# Gather User Variables
################################################################################
# Capture active user username
loggedInUser=$( ls -l /dev/console | awk '{print $3}' )

# Captures active users ID && details about the selectedUser
UserUID=$( id -u "$loggedInUser" )
userRealName=$( /usr/bin/dscl . -read /Users/"${selectedUser}" | /usr/bin/grep RealName: | cut -c11- )
userRecordName=$( /usr/bin/dscl . -read /Users/"${selectedUser}" | /usr/bin/grep RecordName: | cut -c13- )
user_home_location=$( /usr/bin/dscl . -read /Users/"${selectedUser}" NFSHomeDirectory 2>/dev/null | /usr/bin/sed 's/^[^/]*//g' )

# Determine found admin status
if [[ $(/usr/bin/dsmemberutil checkmembership -U "${selectedUser}" -G admin) != *not* ]]; then
    userIsAdmin="yes"
else
    userIsAdmin="no"
fi

################################################################################
# Echo Status
################################################################################
echo "###############################################"
echo "### $selectedUser will be renamed to $jumpcloudUsername ###"
echo "### $jumpcloudUsername is a jumpcloud user with userID: $userID ###"
echo "### This system is jumpcloud bound with ID: $systemID ###"
echo "### $selectedUser is admin: $userIsAdmin ###"
# admin status:
if [[ $admin == 'false' && $userIsAdmin == 'no' ]]; then
    echo "Set Admin status False, Found status False"
    echo "User will remain standard"
elif [[ $admin == 'true' && $userIsAdmin == 'no' ]]; then
    echo "Set Admin status true, Found status False"
    echo "User will become admin"
elif [[ $admin == 'false' && $userIsAdmin == 'yes' ]]; then
    echo "Set Admin status false, Found status true"
    echo "User will be demoted from admin to standard"
elif [[ $admin == 'true' && $userIsAdmin == 'yes' ]]; then
    echo "Set Admin status true, Found status true"
    echo "User will be remain admin"
fi
echo "### $loggedInUser is currently logged in ###"
echo "###############################################"

################################################################################
# migration script begin
################################################################################
echo "Sleep for five seconds"
/bin/sleep 5
# Force logout
echo "Force logout ..."
/bin/ps -Ajc | /usr/bin/grep loginwindow | /usr/bin/awk '{print $2}' | /usr/bin/xargs /bin/kill -9
echo "Sleep for five seconds"
/bin/sleep 5

# Rename local username
echo "Change Local Username Home Directory"
dscl . -change "${user_home_location}" NFSHomeDirectory "${user_home_location}" "/Users/${jumpcloudUsername}"
if [[ $? -ne 0 ]]; then
	echo "Error: Could not rename the user's home directory pointer, aborting further changes! - err=$?"
	echo "Notice: Reverting Home Directory changes" 2>&1
	dscl . -change "/Users/${selectedUser}" NFSHomeDirectory "/Users/${jumpcloudUsername}" "${user_home_location}"
	exit 1
fi
# Rename home directory
echo "Renaming Home Directory"
mv "${user_home_location}" "/Users/${jumpcloudUsername}"
if [[ $? -ne 0 ]]; then
	echo "Error: Could not rename the user's home directory in /Users"
	echo "Notice: Reverting Home Directory changes"
	mv "/Users/${jumpcloudUsername}" "${user_home_location}"
	dscl . -change "/Users/${selectedUser}" NFSHomeDirectory "/Users/${jumpcloudUsername}" "${user_home_location}"
	exit 1
fi
# Rename Username
echo "Rename $selectedUser username to be $jumpcloudUsername"
dscl . -change "${user_home_location}" RecordName "${userRecordName}" "${jumpcloudUsername}"
if [[ $? -ne 0 ]]; then
	echo "Error: Could not rename the user's RecordName in dscl - the user should still be able to login, but with user name ${selectedUser}"
	echo "Notice: Reverting username change"
	dscl . -change "/Users/${selectedUser}" RecordName "${jumpcloudUsername}" "${selectedUser}"
	echo "Notice: Reverting Home Directory changes"
	mv "/Users/${jumpcloudUsername}" "${user_home_location}"
	dscl . -change "/Users/${selectedUser}" NFSHomeDirectory "/Users/${jumpcloudUsername}" "${user_home_location}"
	exit 1
fi
# Create local user account ...
echo "Takeover ${selectedUser} local account with JumpCloud Agent"
userBind=$(
    curl -s \
        -X 'POST' \
        -H 'Accept: application/json' \
        -H 'Content-Type: application/json' \
        -H 'x-api-key: '${jumpcloudAPIKey}'' \
        -d '{ "attributes": { "sudo": { "enabled": '${admin}',"withoutPassword": false}}   , "op": "add", "type": "user","id": "'${userID}'"}' \
        "https://console.jumpcloud.com/api/v2/systems/${systemID}/associations"
)

# wait a moment at login screen
# Wait for user updates to complete
# Get JCAgent.log to ensure user updates have been processes on the system
logLinesRaw=$(wc -l /var/log/jcagent.log)
logLines=$(echo $logLinesRaw | head -n1 | awk '{print $1;}')
groupSwitchCheck=$(sed -n ''${logLines}',$p' /var/log/jcagent.log)
groupTakeOverCheck=$(echo ${groupSwitchCheck} | grep "User updates complete")

# Get current Time
now=$(date "+%y/%m/%d %H:%M:%S")
# Get last line of the JumpCloud Agent
lstLine=$(tail -1 /var/log/jcagent.log)
regexLine='([0-9][0-9])-([0-9][0-9])-([0-9][0-9]) ([0-9][0-9]:[0-9][0-9]:[0-9][0-9])'
if [[ $lstLine =~ $regexLine ]]; then
    # Get the time form the agent log
    lstTime=${BASH_REMATCH[0]}
fi
echo "$(date "+%Y-%m-%d %H:%M:%S"): Current System Time       : $now"
echo "$(date "+%Y-%m-%d %H:%M:%S"): JCAgent Last Log Time     : $lstTime"
# convert to Epoch time
nowEpoch=$(date -j -f "%y/%m/%d %T" "${now}" +'%s')
jclogEpoch=$(date -j -f "%y-%m-%d %T" "${lstTime}" +'%s')
echo "$(date "+%Y-%m-%d %H:%M:%S"): Current System Epoch Time : $nowEpoch"
echo "$(date "+%Y-%m-%d %H:%M:%S"): Last JCAgent Epoch Time   : $jclogEpoch"
# get the difference in time
epochDiff=$(( (nowEpoch - jclogEpoch) ))
echo "$(date "+%Y-%m-%d %H:%M:%S"): Difference between logs is: $epochDiff seconds"

# Check the JCAgent log, it should check in under 180s
while [[ $epochDiff -le 180 ]]; do
    # wait a second and update all the variables
    Sleep 1
    groupSwitchCheck=$(sed -n ''${logLines}',$p' /var/log/jcagent.log)
    groupTakeOverCheck=$(echo ${groupSwitchCheck} | grep "Processing user updates")
    lstLine=$(tail -1 /var/log/jcagent.log)
    regexLine='([0-9][0-9])-([0-9][0-9])-([0-9][0-9]) ([0-9][0-9]:[0-9][0-9]:[0-9][0-9])'
    if [[ $lstLine =~ $regexLine ]]; then
        lstTime=${BASH_REMATCH[0]}
    fi
    jclogEpoch=$(date -j -f "%y-%m-%d %T" "${lstTime}" +'%s')
            now=$(date "+%y/%m/%d %H:%M:%S")
            nowEpoch=$(date -j -f "%y/%m/%d %T" "${now}" +'%s')
            epochDiff=$(( (nowEpoch - jclogEpoch) ))
    # if the log is empty continue while loop
    if [[ -z $groupTakeOverCheck ]]; then
        echo "$(date "+%Y-%m-%d %H:%M:%S"): Waiting for log sync, JCAgent last log was: $epochDiff seconds ago"
        echo "$(date "+%Y-%m-%d %H:%M:%S"): JCAgent last line: $lstLine"
    else
        now=$(date "+%y/%m/%d %H:%M:%S")
        # log found, break out of the while loop
        echo "$(date "+%Y-%m-%d %H:%M:%S"): Log Synced! User Updates Complete $now"
        # echo "$(date "+%Y-%m-%d %H:%M:%S"): $groupTakeOverCheck" >>"$DEP_N_DEBUG"
        break
    fi
    # # if the time difference is greater than 90 seconds, restart the JumpCloud agent to begin logging again
    # if [[ $epochDiff -eq 90 ]]; then
    #     echo "$(date "+%Y-%m-%d %H:%M:%S"): JumpCloud not reporting local account takeover"
    #     echo "$(date "+%Y-%m-%d %H:%M:%S"): Waking the JumpCloud Agent"
    #     /opt/jc/bin/jumpcloud-agent
    # fi
done

# Links old home directory to new. Fixes dock mapping issue
ln -s "/Users/${jumpcloudUsername}" "${user_home_location}"

# create the plist file:
echo '#!/bin/bash
/sbin/shutdown -r +1
' > /tmp/reboot.sh
# make the script executable
chmod +x /tmp/reboot.sh
# create the plist file:
echo '<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>Label</key>
	<string>com.jumpcloud.prestage</string>
	<key>Program</key>
    <string>/tmp/reboot.sh</string>
	<key>RunAtLoad</key>
	<true/>
    <key>StandardOutPath</key>
    <string>/var/tmp/com.apple.restart.stdout</string>
    <key>StandardErrorPath</key>
    <string>/var/tmp/com.apple.restart.stderr</string>
</dict>
</plist>' > /Library/LaunchDaemons/com.rebootnow.plist

# change ownership
chown root:wheel "/Library/LaunchDaemons/com.rebootnow.plist"
chmod 644 "/Library/LaunchDaemons/com.rebootnow.plist"

launchctl load -w /Library/LaunchDaemons/com.rebootnow.plist

sleep 2
# remove the LaunchDaemons so that they do not kick off after the restart
rm /Library/LaunchDaemons/com.rebootnow.plist
## End Script ##
exit 0
Tags:
[ powershell  bash  accounts  automation  ]