Shared User Profiles – Staging Scripts
by dwarfsoft on Mar.15, 2010, under Novell, Scripting, Tweet, Work
As promised, here are the scripts required for the Pre-staging of Domain User Profiles on the local machine. The first thing we need to do is Enumerate all the Local User Accounts.
Function StageAllUsers(DomainFQDN, strDomain) ' Enumerate all users that are Local and not built in accounts. strComputer = "." Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2") 'Enumerate users where the User Domain is the Local Machine Set colItems = objWMIService.ExecQuery _ ("Select * from Win32_UserAccount " & _ "Where Domain = '" & GetComputerName & "' " & _ "And Disabled = FALSE And Name <> 'Administrator'") ' Stage each user For Each objItem In colItems ' Ensure the account actually has a profile (otherwise we can ignore it) If GetLocalUserProfile(objItem.Name) <> "" Then ret = StageUser(objItem.Name, DomainFQDN, strDomain) End If Next End Function
The functions called here are GetComputerName, which returns the name of the local machine, and the other important ones are GetLocalUserProfile and Stage User. The first we can check is GetLocalUserProfile.
'Gets the Profile Path for the User passed in UserName. ' This requires the profile to exist on the local machine ' Returns an empty string on error Function GetLocalUserProfile(UserName) On Error Resume Next GetLocalUserProfile = "" SDDL = GetLocalUserSDDL(UserName) If SDDL = "" Then Exit Function End If Set objShell = CreateObject("WScript.Shell") GetLocalUserProfile = objShell.ExpandEnvironmentStrings( _ objShell.RegRead( _ "HKLM\Software\Microsoft\Windows NT\CurrentVersion\ProfileList\" & _ SDDL & _ "\ProfileImagePath")) On Error Goto 0 End Function
The registry path (including the SDDL, covered next) returns the profile path for the user. This relies on a function called GetLocalUserSDDL which goes as follows:
Function GetLocalUserSDDL(UserName) strComputer = "." Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2") 'Enumerate users where the User Domain is the Local Machine 'and the username matches the Supplied Name Set colItems = objWMIService.ExecQuery _ ("Select * from Win32_UserAccount " & _ "Where Domain = '" & GetComputerName & "' " & _ "And Name = '" & UserName & "'") GetLocalUserSDDL = "" For Each objItem In colItems 'Get the first returned SID/SDDL then exit GetLocalUserSDDL = objItem.SID Exit Function Next End Function
This reads the SDDL (eg S-1-5-21-791012361-4073638415-1907800938-1006 or otherwise known as the String SID) for the User on the local machine. The next major function is Stage User:
Function StageUser(UserName, DomainFQDN, strDomain) Set objShell = CreateObject("WScript.Shell") ' Not Assuming "C:\Documents and Settings\" & UserName: ' Get SDDL of user via WMI interrogation of UserAccount ' Get Profile Location of user (from HKLM\Software\Microsoft\Windows NT\CurrentVersion\ProfileList\" & SDDL & "ProfileImagePath" strProfile = GetLocalUserProfile(UserName) ' Set ACLs on ProfileLocation ret = objShell.Run("subinacl /subdirectories """ & strProfile & """ /grant=""" & UserName & "@" & DomainFQDN & """=F",0,True) ' Set ACLs on D:\UserData\%username% ret = objShell.Run("subinacl /subdirectories ""D:\UserData\" & UserName & """ /grant=""" & UserName & "@" & DomainFQDN & """=F",0,True) ' Reg load ProfileLocation\NTUSER.DAT into HKU\%username% ret = objShell.Run("reg load ""HKU\" & UserName & """ """ & strProfile & "\NTUSER.DAT""" ,0,True) ' Reg load ProfileLocation\Local Settings\Application Data\Microsoft\Windows\UsrClass.DAT into HKU\%username%_Classes ret = objShell.Run("reg load ""HKU\" & UserName & "_Classes"" """ & strProfile & "\Local Settings\Application Data\Microsoft\Windows\UsrClass.DAT""" ,0,True) 'Try both of the following. One should fail, the other should pass. ' Set ACLs on HKU\%username% ret = objShell.Run("subinacl /subkeyreg ""HKEY_USERS\" & UserName & """ /grant=""" & UserName & "@" & DomainFQDN & """=F",0,True) ' If User is already logged in then the registry will be open at HKU\SDDL ret = objShell.Run("subinacl /subkeyreg ""HKEY_USERS\" & GetLocalUserSDDL(UserName) & """ /grant=""" & UserName & "@" & DomainFQDN & """=F",0,True) 'Try both of the following. One should fail, the other should pass. ' Set ACLs on HKU\%username% ret = objShell.Run("subinacl /subkeyreg ""HKEY_USERS\" & UserName & "_Classes"" /grant=""" & UserName & "@" & DomainFQDN & """=F",0,True) ' If User is already logged in then the registry will be open at HKU\SDDL ret = objShell.Run("subinacl /subkeyreg ""HKEY_USERS\" & GetLocalUserSDDL(UserName) & "_Classes"" /grant=""" & UserName & "@" & DomainFQDN & """=F",0,True) ' Reg unload HKU\%username% ret = objShell.Run("reg unload ""HKU\" & UserName & """",0,True) ' Reg unload HKU\%username%_Classes ret = objShell.Run("reg unload ""HKU\" & UserName & "_Classes""",0,True) ' Get Domain User SID from ProfileLocation ACL arrSID = GetDomainUserSidFromFolderACL(strProfile, UserName, strDomain) If UBound(arrSID) > 0 Then ' Convert Domain User SID to SDDL WScript.Echo "Getting Sid for " & UserName SDDL = ArraySidToStrSid(arrSID) ' Stage Domain User Profile into Registry under HKLM\Software\Microsoft\Windows NT\CurrentVersion\ProfileList\%DomainUserSDDL% ret = CreateAndLinkProfile(SDDL, arrSID, strProfile) Else 'Account didn't exist in the domain or ACL failed to set on Folder End If End Function
For the most part this function behaves by running external commands to mount registry hives, and set ACLs on folders and registry, all up until we call GetDomainUserSidFromFolderACL. This is where we retrieve the SID of the Domain User that we are pre-staging via the ACL we set earlier on the Local User Profile folder. This subverts the need to query Active Directory for the SID through other means.
Function GetDomainUserSidFromFolderACL(strFolder, strUserName, strUserDomain) Dim arrSID(0) GetDomainUserSidFromFolderACL = arrSID Set objFSO = CreateObject("Scripting.FileSystemObject") If Not objFSO.FolderExists(strFolder) Then Set objFSO = Nothing Exit Function End If ' Ensure that strFolder is of the form C:\\Documents and Settings\\UserName strFolderName = Replace(strFolder, "\\","\") strFolderName = Replace(strFolderName, "\","\\") Set wmiFileSecSetting = GetObject( _ "winmgmts:Win32_LogicalFileSecuritySetting.path='" & strFolderName & "'") RetVal = wmiFileSecSetting. _ GetSecurityDescriptor(wmiSecurityDescriptor) If Err <> 0 Then Exit Function End If ' Retrieve the DACL array of Win32_ACE objects. DACL = wmiSecurityDescriptor.DACL For each wmiAce in DACL ' Get Win32_Trustee object from ACE Set Trustee = wmiAce.Trustee If (StrComp(Trustee.Name, strUserName, vbTextCompare) = 0) And _ (StrComp(Trustee.Domain, strUserDomain, vbTextCompare) = 0) Then GetDomainUserSidFromFolderACL = Trustee.SID Exit Function End If Next End Function
Although this subverts the need to Query Active Directory, this does also mean that we receive the SID in a format that is unexpected. Normally, via querying Active Directory we would have received a SID in an Octet String format. Querying the SID from WMI returned it in SDDL or String SID format. The format that we receive the SID from the ACL is in an Array of Integers. In order to Pre-stage an account we need the SDDL/String SID, and the only way to get this in VBScript is to manually convert it using some functions developed by Richard Mueller and Wilfred Wong in this newsgroup posting.
Instead of using only the code listed, we need to alter it for dealing with the SID array we received back from the ACL.
Function ArraySidToStrSid(arrSid) ' Function to convert OctetString (byte array) to Decimal string (SDDL) Sid. Dim strHex, strDec strHex = ArraySidToHexStr(arrSid) strDec = HexStrToDecStr(strHex) ArraySidToStrSid = strDec End Function Function ArraySidToHexStr(arrSid) ArraySidToHexStr = "" For Each x In arrSid ArraySidToHexStr = ArraySidToHexStr & _ Right("0" & Hex(x), 2) Next End Function
The HexStrToDecStr function is the same as it was from the Richard Mueller posting listed above.
Finally we just need to run through the actual staging of the account:
Function CreateAndLinkProfile(strNewSDDL, strNewSID, strProfilePath) WScript.Echo "Creating Profile for " & strNewSDDL & ": " & strProfilePath 'Write Sid to registry ret = WriteRegBinaryToRegistry(HKEY_LOCAL_MACHINE,"SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\" & strNewSDDL,"Sid",strNewSID) ret = WriteRegStringToRegistry(HKEY_LOCAL_MACHINE,"SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\" & strNewSDDL,"ProfileImagePath",strProfilePath) ret = WriteRegStringToRegistry(HKEY_LOCAL_MACHINE,"SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\" & strNewSDDL,"CentralProfile","") ret = WriteRegDwordToRegistry(HKEY_LOCAL_MACHINE,"SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\" & strNewSDDL,"Flags",1) ret = WriteRegDwordToRegistry(HKEY_LOCAL_MACHINE,"SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\" & strNewSDDL,"State",0) End Function
At this point if the Domain User logs in they get redirected to the existing local users profile. Then if rollback is initiated they log on with the Local User account with the same profile. This is seamless to end users, both forward and backward.
On a side note the functions for writing to the registry are through WMI due to needing to write Binary values to the Registry
Function WriteRegBinaryToRegistry(Hive, strKeyPath, strValueName, ArrValues) strComputer = "." Set oReg = GetObject("winmgmts:{impersonationLevel=impersonate}!\\" & _ strComputer & "\root\default:StdRegProv") oReg.CreateKey Hive,strKeyPath oReg.SetBinaryValue Hive, strKeyPath, strValueName,ArrValues End Function Function WriteRegDwordToRegistry(Hive, strKeyPath, strValueName, Value) strComputer = "." Set oReg = GetObject("winmgmts:{impersonationLevel=impersonate}!\\" & _ strComputer & "\root\default:StdRegProv") oReg.CreateKey Hive,strKeyPath oReg.SetDwordValue Hive, strKeyPath, strValueName,Value End Function Function WriteRegStringToRegistry(Hive, strKeyPath, strValueName, Value) strComputer = "." Set oReg = GetObject("winmgmts:{impersonationLevel=impersonate}!\\" & _ strComputer & "\root\default:StdRegProv") oReg.CreateKey Hive,strKeyPath oReg.SetStringValue Hive, strKeyPath, strValueName,Value End Function
Cheers, Chris.
Edit: DomainAccountProfileStaging.vbs has been uploaded as a complete file.
8 Comments for this entry
1 Trackback or Pingback for this entry
-
Last Login Time - Dwarfsoft [GPA]
April 22nd, 2010 on 11:54 am[...] the issue manually. As I have had a fair bit of experience with Windows Profiles recently (see the last blog post) I was familiar with the ProfileLoadTimeHigh and ProfileLoadTimeLow registry keys in the [...]

May 1st, 2010 on 4:30 am
I am interested in using this script, as I work for a school district who is migrating from Novell to AD but I’m not sure how to put it together and how to run it. Could you provide some more info?
Thanks,
Dustin Roberts
May 3rd, 2010 on 11:28 am
Hi,
Well, to put the script together you simply create a VBScript file that includes all the functions, then call StageAllUsers passing the domain FQDN and the Netbios Domain.
From there you simply package it so it installs on the Workstations, then get it to run after every logon. This way the domain accounts and the local accounts are synchronized.
I have uploaded the VBS script into the original Article now.
If, however, you do not need to worry about rollback then you can simply use the User State Migration Tools that Microsoft has provided to convert the user accounts to Domain accounts (which is much simpler).
Cheers, Chris.
March 15th, 2012 on 12:52 am
Many thanks for the excellent scripts. They work really well. On some of our devices, we can have up to 50 local user profiles. This means that the script can take a long time to run. Is there a way to alter the script so that it would only migrate accounts that have a last login time of 60 days or less. I have had a good attempt but have not been successful. Thanks.
March 15th, 2012 on 6:48 am
I covered load time of profiles here: http://www.dwarfsoft.com/blog/2010/04/22/last-login-time/
The Script you would want is:
Restructure and throw that in a function that instead of finding the current user uses whatever ‘Subkey’ you pass to it and returns the last login time. That should be quick as it need only do a couple of registry reads and some math. If I had time I’d script it up, but everything you need is here already
Then you can do a DateDiff between now and the returned date.
Cheers, Chris.
March 15th, 2012 on 7:01 am
Many thanks for responding. I shall test the script and then insert it into the main staging script. So far the staging script has been really great. It has solved our issue in moving the DLU accounts to AD accounts on the devices.
March 15th, 2012 on 8:00 pm
Thanks for the feedback. I’m so glad to see that this actually reached production somewhere. The project that I produced this for stagnated and I left the company and moved on without ever seeing it work on a large scale (80,000 End users, probably about 20,000 PCs/Laptops).
This script could have been modified so that it only staged the DLU profiles once, thereby making it run through quicker with each subsequent attempt. You could just put a Registry value into the ProfileList\{GUID} path and manage this, or in any other place in the registry that suits for that matter (HKLM\Software\ProfileStaging\userid).
Thanks for the feedback.
Cheers, Chris.
March 18th, 2012 on 7:05 pm
I did modify the script to improve some of the coding, which has speeded the process up. Overall though it has been excellent and well done in writing it originally. We tested it in a pre-prod environment fully before going into production. We are quite large, 15,0000 devices and 30,000 users. So far we have done over 8,000 devices with not many issues at all. Support are pleased with it as if there are any issues, they can fix it quite easily. I’m just modifying the script to check for the last login time of the profiles, as we will be using the script on some of our very slow WAN sites soon. Many thanks.
March 18th, 2012 on 7:06 pm
Too many zeros, 15,000 devices!