Vista’s MoveUser.exe replacement

Vista’s MoveUser.exe replacement

  • Comments 24
  • Likes

Hi Rob here again. I recently had a customer that needed the functionality of MoveUser.exe from the Windows 2000 Resource Kit available in Windows Vista. The customer had quite a few Windows Vista machines that were not joined to the domain but were now migrating to Active Directory. For their own business reasons they were previously unable to join the machines to the domain, instead all the users logged on with local user accounts. Since they had this new fancy Active Directory they created user accounts in AD for the users and joined the machines to the domain. They then found that they needed a way to attach the users’ existing local profile to their Active Directory user accounts so that the users would have their normal setup and desktop when they logged in-a seamless experience. Now if you have been around forever like most of us here in support in Windows 2000 and up we used a utility named MoveUser.exe to accomplish this.

Well, in Windows Vista moveuser.exe is no longer supported. However, now we expose this functionality with a new WMI provider called Win32_UserProfile, which is discussed in KB930955. This is awesome because we expose things about user profiles in WMI now… and we can also move the profile to another user as well as delete user profiles. However, once I started looking at MSDN to understand what methods were available I quickly found that MSDN has not been updated as of yet for this new class. So we did some digging into the source code to find out how this works and what is supported.

I wrote a sample script that illustrates how you can leverage this provider to move an existing user profile to another user’s profile. I know that I could have made the script smaller by not listing out all the different properties available in the provider, but the different things exposed are just way too many and if you are planning on using this provider they are just way too cool.

Usage

Please keep in mind that this is a sample script-you will need to alter it and test it in your environment and for your needs. To use:

1. Copy the below script into Notepad then save as moveuser.vbs

2. You will need to modify the following variables within the script.

  • strComputer: The computer name that this script needs to run against
  • strSourceAcct: The user account that has the source profile on the system
  • strSourceAcctDomain: The domain of the source user account that the profile belongs to. If the source account that you want to move the profile from is a local computer user you put in the computers name for the domain. If this is another domain then you type in the domain name.
  • strTargetAcct: The user account that the source profile should be moved to.
  • strTargetDomain: The domain of the target user account that the profile should be moved to. If the target account that you want to move the profile to is a local computer user you put in the computers name. If this is another domain then you type in the domain name.
  • strDomainDN: The Target Account Domains Distinguished Name. This is done for the LDAP query to be built to find the target accounts SID. for example dc=contoso,dc=com

3. Run the script by typing cscript moveuser.vbs

Sample Script

'     This script is provided "AS IS" with no warranties, and confers no rights.
'     For more information please visit
'    
http://www.microsoft.com/info/cpyright.mspx to find terms of use.
'
Option Explicit
DIM strComputer, strSourceAcct, strSourceAcctDomain, strTargetAcct
DIM strTargetAcctDomain, strTargetAcctSID
DIM objProfile, objCommand, objRecordSet, objConnection, objWMIService, objSID
DIM dtStart, colProfiles, oSID, oUsr
DIM Revision, IssueAuthorities(11), strSDDL, subAuthorities
DIM strDomainDN

CONST ADS_SCOPE_SUBTREE=2

'  This script has hard coded variables in it that must be filled out.
'  strComputer = The computer name that this script needs to run against.
'                                   With WMI the "." means this computer.
'
'  strSourceAcct = user account that has the source profile on the system.
'
'  strSourceAcctDomain = The domain of the source user account that the profile belongs to.
'                                   If the source account that you want to move the profile from
'                                   is a local computer user you put in the computers name for the domain. 
'                                   If this is another domain then you type in the domain name.
'
' strTargetAcct = The user account that the source profile should be moved to.
'
' strTargetDomain = The domain of the target user account that the profile should be moved to
'                                   If the target account that you want to move the profile to.
'                                   is a local computer user you put in the computers name. 
'                                   If this is another domain then you type in the domain name.
'
' strDomainDN = The Target Account Domains Distinguished Name. 
'                                  This is done for the LDAP query to be built to find the target accounts SID
'

strComputer ="."
strSourceAcct="User1"
strSourceAcctDomain="Contoso-Vista"
strTargetAcct="User1"
strTargetAcctDomain="CONTOSO"
strDomainDN="dc=contoso,dc=com"
strTargetAcctSID=""
dtStart = TimeValue(Now())
Set objConnection = CreateObject("ADODB.Connection")
objConnection.Open "Provider=ADsDSOObject;"
Set objCommand = CreateObject("ADODB.Command")
objCommand.ActiveConnection = objConnection

' We need the proper Active Directory domain name where the user exists in a DN format.  You can
' modify the strDomainDN variable to you Active Directory domain name is in DN format.

objCommand.CommandText = _
    "SELECT AdsPath, cn FROM 'LDAP:// "+strDomainDN+"' WHERE objectCategory = 'user'" & _
         "And sAMAccountName= '"+strTargetAcct+"'"
objcommand.Properties("searchscope") = ADS_SCOPE_SUBTREE 
Set objRecordSet = objCommand.Execute
If objRecordset.RecordCount = 0 Then
    WScript.Echo "sAMAccountName: " & strTargetAcct & " does not exist."
ElseIf objRecordset.RecordCount > 1 Then
    WScript.Echo "There is more than one account with the same sAMAccountName"
Else
    WScript.Echo "Found account:  "+strTargetAcctDomain+"\"+strTargetAcct + " in the domain."
    objRecordSet.MoveFirst
    Do Until objRecordSet.EOF
        Set Ousr = GetObject(objRecordSet.Fields("AdsPath").Value)
        strTargetAcctSID = SDDL_SID(oUsr.Get("objectSID"))
        WScript.echo "SID for  "+ strTargetAcctDomain+"\"+strTargetAcct + _
" is:  "+strTargetAcctSID
        WScript.Echo VBNewLine
        WScript.Echo VBNewLine

        objRecordSet.MoveNext
    Loop

objConnection.Close

Set objWMIService = GetObject("winmgmts:\\" & strComputer &"\root\cimv2")
Set colProfiles = objWMIService.ExecQuery("Select * from Win32_UserProfile")
For Each objProfile in colProfiles
    Set objSID = objWMIService.Get("Win32_SID.SID='" & objProfile.SID &"'")
    Wscript.Echo"======================================================"& VBNewLine _
        &"Sid:" & objProfile.Sid & VBNewLine _
        &"User Name:" & objSID.AccountName & VBNewLine _
        &"User Domain:" & objSID.ReferencedDomainName & VBNewLine _
        &"LocalPath:" & objProfile.LocalPath & VBNewLine _
        &"Loaded:" & objProfile.Loaded & VBNewLine _
        &"RefCount:" & objProfile.RefCount & VBNewLine _
        &"RoamingConfigured:" & objProfile.RoamingConfigured & VBNewLine _
        &"RoamingPath:" & objProfile.RoamingPath & VBNewLine  _
        &"RoamingPreference:" & objProfile.RoamingPreference & VBNewLine _
        &"Status:" & objProfile.Status & VBNewLine _
        &"LastUseTime:" & objProfile.LastUseTime & VBNewLine  _
        &"LastDownloadTime:" & objProfile.LastDownloadTime & VBNewLine  _
        &"LastUploadTime:" & objProfile.LastUploadTime & VBNewLine 

'    Testing to verify that the current profile handle is for the Source Account that we want to
'    Move to the domain user.
if UCase(objsid.referencedDomainName+"\"+objsid.AccountName)= _ 
UCase(strSourceAcctDomain+"\"+strSourceAcct) Then
    ' Making sure that the source profile is currently not in use.  If it is we will bail out.
        If objProfile.RefCount < 1 Then
WScript.echo "Change Profile for:  "+ strSourceAcctDomain+"\"+ _
strSourceAcct+" to: "+ strTargetAcctDomain+"\"+strTargetAcct
        ' ChangeOwner method requires to String SID of Target Account and a Flag setting

        ' Flag 1 = Change ownership of the source profile to target account
' even if the target account  already has a profile on the system.

        ' Flag 2 = Delete the target account Profile and change ownership
'  of the source user account profile to the target account.

        '  To use the ChangeOwner method, both the source and
'  target account profiles (If it exists) must not be loaded.

            ObjProfile.ChangeOwner strTargetAcctSID,1
     Else 
            Wscript.echo "Could not move the users profile, because " + _
            strSourceAcctDomain+"\"+strSourceAcct+" profile is currently loaded"
        End If   
     End If
Next
End If
Sub Init_IssueAuthorities( )
    'DIM IssueAuthorities(11)
    IssueAuthorities(0) = "-0-0"
    IssueAuthorities(1) = "-1-0"
    IssueAuthorities(2) = "-2-0"
    IssueAuthorities(3) = "-3-0"
    IssueAuthorities(4) = "-4"
    IssueAuthorities(5) = "-5"
    IssueAuthorities(6) = "-?"
    IssueAuthorities(7) = "-?"
    IssueAuthorities(8) = "-?"
    IssueAuthorities(9) = "-?"

end sub

function SDDL_SID ( oSID )
    DIM Revision, SubAuthorities, strSDDL, IssueIndex, index, i, k, p2, subtotal
    DIM j, dblSubAuth
'
' First byte is the revision value
'
    Revision = "1-5"
'
' Second byte is the number of sub authorities in the
' SID
'
    SubAuthorities = CInt(ascb(midb(oSID,2,1)))
    strSDDL = "S-" & Revision
    IssueIndex = CInt(ascb(midb(oSID,8,1)))
'
' BYtes 2 - 8 are the issuing authority structure
' Currently these values are in the form:
' { 0, 0, 0, 0, 0, X}
'
' We use this fact to retrieve byte number 8 as the index
' then look up the authorities for an array of values
'
    strSDDL = strSDDL & IssueAuthorities(IssueIndex)
'
' The sub authorities start at byte number 9. The are 4 bytes long and
' the number of them is stored in the Sub Authorities variable.
'
    index = 9
    i = index
    for k = 1 to SubAuthorities
        '
        ' Very simple formula, the sub authorities are stored in the
        ' following order:
        ' Byte Index Starting Bit
        ' Byte 0 - Index 0
        ' Byte 1 - Index + 1 7
        ' Byte 2 - Index + 2 15
        ' Byte 3 - Index + 3 23
        ' Bytes0 - 4 make a DWORD value in whole. We need to shift the bits
        ' bits in each byte and sum them all together by multiplying by powers of 2
        ' So the sub authority would be built by the following formula:
        '
        ' SUbAuthority = byte0*2^0 + Byte1*2^8 + byte2*2^16 + byte3*2^24
        '
        ' this be done using a simple short loop, initializing the power of two
        ' variable ( p2 ) to 0 before the start an incrementing by 8 on each byte
        ' and summing them all together.
        '
        p2 = 0
        subtotal = 0
        for j = 1 to 4
        dblSubAuth = CDbl(ascb(midb(osid,i,1))) * (2^p2)
        subTotal = subTotal + dblSubAuth
        p2 = p2 + 8
        i = i + 1
    next
'
' Convert the value to a string, add it to the SDDL Sid and continue
'
    strSDDL = strSDDL & "-" & cstr(subTotal)
    next
    SDDL_SID = strSDDL
end function

Please keep in mind if you have not installed Service Pack 1 for Vista, you will need to download the MSI installer to get the new WMI Profile provider since it was released after Vista shipped.

Well, I hope that you find this functionality helpful. Happy scripting.

- Rob Greene

  • I ran into a few problems trying to use this script.  First, the variable strDomainDN was not defined in your DIM statements. Second, the quote marks on line 41 - strDomainDN=”dc=contoso,dc=com” are the wrong type and generate an error.  And third, on line 117 you have the "_" to indicate that the command is continued on the next line, but there is no carraige return so the script errors out again there.  Once I changed those three items and edited the appropiate variables for the user I was wanting to move, the script ran - and I saw the info for all the users scroll through the command window - However, the user's profile wasn't moved.  When I logged in using the AD account, it created a new profile rather than using the one I tried to move.  What I put in for variable looks mostly like this:

    strComputer ="."

    strSourceAcct="fred"

    strSourceAcctDomain="Test-MigVista01"

    strTargetAcct="wilsonf"

    strTargetAcctDomain="UNIVERSITY"

    strDomainDN="dc=university,dc=edu"

    The script runs without errors but the profile doesn't seem to move.  But when I log the computer in using 'wilsonf@university.edu' it generates a new profile.

    Am I missing something?

    ==Ron.

  • Rob's taking a look at this now; once he irons it all out I will update the main post. Thanks for writing all this up Ron!

    - Ned

  • Ok folks, try it again. The script got a bit tanked up by the blogging formatting, but the two functional errors should be fixed. It was not actually correctly building the full SID string, due to over-engineering. :)

    - Ned

  • That fixed it.  Migrated the profile perfectly, just like the old moveuser.exe did with XP.

    Thank you very much.  I really needed a script like this right now - and from what I found on the 'net, lot's of other people are looking for it, as well.

    ==Ron.

  • That's great Ron, glad it worked for you. :)

    - Ned

  • By adding the ability to accept command line arguments, and a few lines of code to process them, I made the script function nearly identically to the previous exe version - and I don't have to edit the script every time I run it. ( you still have to edit one master variable - strDomainDN="dc=Contoso,dc=com"

    and make it whatever your domain is - but that would probably be a one-time edit for most admins.)  So now, I can launch the command prompt (as admin, of course) and simply type:

     Moveuser user1 DOMAIN\user1

    just as before. (this way runs with wscript instead of cscript but it still works.)

    Here is what I added:

    '/========================

    DIM strDomAcctLength, strCompName, strSlashPos

    DIM strDomainAcct, strLocalAcct, strLocalAcctLength

    If WScript.Arguments.Count = 2 Then

    strLocalAcct = WScript.Arguments.Item(0)

    strDomainAcct = WScript.Arguments.Item(1)

     Else

      Wscript.Echo "Usage 1: Moveuser.vbs LocalUsername Domain\Username" _

      & VBNewLine & "or..." _

      & VBNewLine & "Usage 2: Moveuser.vbs Domain\Username Domain\Username"

      Wscript.Quit

    End If

    'Process arg 1'

    If InStr (strLocalAcct, "\") = 0 Then

     Set strCompName = WScript.CreateObject("WScript.Network")

     strSourceAcctDomain = strCompName.ComputerName

     strSourceAcct = strLocalAcct

     Else

       strLocalAcctLength = Len(strLocalAcct)

       strSlashPos = InStr (strLocalAcct, "\")

       strSourceAcctDomain = Left(strLocalAcct,strSlashPos - 1)

       strSourceAcct = Right(strLocalAcct,strLocalAcctLength - strSlashPos)

    End If

     'Process arg 2'

     strDomAcctLength = Len(strDomainAcct)

     strSlashPos = InStr (strDomainAcct, "\")

     strTargetAcctDomain = Left(strDomainAcct,strSlashPos - 1)

     strTargetAcct = Right(strDomainAcct,strDomAcctLength - strSlashPos)

    '/=== the lines below resume from the original ===

    strComputer ="."

    strDomainDN="dc=Contoso,dc=com"

    strTargetAcctSID=""

  • Hey, that's great Ron! I tried to pummel Rob into doing that as well but he had other things going on and took the easy way out. :)

    Thanks very much for providing that sample for people here, I'm sure they will find it useful.

  • I ran across another problem...  When I try to run the script to change an account from a Domain to a local user (instead of the other way around...), I get the following error:

    (116, 13) SWbemObjectEx: The parameter is incorrect.

    What I have for the variables is similar to before, but in reverse:

    strComputer ="."

    strSourceAcct="wilsonf"

    strSourceAcctDomain="UNIVERSITY"

    strTargetAcct="fred"

    strTargetAcctDomain="Test-MigVista20"

    strDomainDN="dc=university,dc=edu"

    It's almost as if the "ObjProfile.ChangeOwner" doesn't work in this direction.

    Any ideas?

  • I'd be very suspicious of that ever working - it's not something I've ever seen anyone ask for, which means the product group that designed that function probably didn't consider it (usually the reverse, no one gets rid of AD; they are trying to get rid of workgroups). I'll see if I can get a definitive answer on that for you.

    - Ned

  • Where Rob spells out the description for each of the variables above, it "sounds" like it should work.  Not sure if it worked with the old Moveuser.exe or not.  Perhaps I'll test that out...

  • Just tested the Moveuser.exe under XP and was able to successfully revert a user from the domain back to being a local user.

  • Hey Ron,

    So the problem here is not functionality of the WMI Provider.  The issue here is the script.  

    So the issue with the script is that it is hard coded to look at active directory for the Target account.

    If you read the comments around the objProfile.ChangeOwner method in the script it requires a string representation of the users SID i.e. "S-1-5-21....." then it needs a flag setting.

    =============================

          ' ChangeOwner method requires to String SID of Target Account and a Flag setting

           ' Flag 1 = Change ownership of the source profile to target account even if the target account  already has a profile on the system.

           ' Flag 2 = Delete the target account Profile and change ownership of the source user account profile to the target account.

    ==============================

    The script shown above assumes that the target account is a domain based account, since that is what most customers reqire.  That is why the script does an LDAP query for the user in question and gets a HEX representation of the users SID.  It then converts the HEX SID to a Text SID.

    So here is the rub, the script would have to be modified to look at the local SAM acocunt database and search it for the users SID to make it work.  Otherwise if you know off the top of you head the users SID you can use that as the first parameter in the method.

    Now you could connect to the WINNT provider to find the users local account or the WMI provider of Win32_UserAccount

    http://msdn.microsoft.com/en-us/library/aa394507.aspx

    I hope that this helps.

    Rob Greene

  • Thanks for the info.  I understand now why it doesn't work in the reverse direction.  Now I need to decide if I want to go through the work of adding that functionality for something that really will rarely (if ever) be used.

  • Hey Ron,

    I totally understand that.  However, if you do choose to do this we would be glad to have the code added to the comments sectoin  ;)

  • I might tackle this later...  I've been editing the script to make it closer to the old MoveUser.exe because we are going to begin a 'mass migration' next week, converting ~600 remaining users from Novell to AD.  I wanted my instructions for the team to be as close to identical for both XP and Vista as I could get them.  I've made so many changes now that simply posting a code-snip wouldn't work very well.  I'm telling our team to get the file from:

    http://tacklebox.cns.ohiou.edu/Moveuser/

    If you want to check out the changes in the file, let me know what you think... (and if you see any problems that I've missed.)

    Converting profiles back really isn't an issue right now - but in a University environment we get piles of requests that make all of us say "Why would they want to do that?" - But we aren't generally able to say 'no' unless it's trully imposible (and sometimes not even then...) So while I would like to have the 'ability' to move them back, I don't have the time to write and test that before we begin. And I doubt I'll have the need right away.

    So,... here we go........