904 lines
26 KiB
Plaintext
904 lines
26 KiB
Plaintext
' clonepr.vbi start
|
|
|
|
// VB Script "Include" file for CloneSecurityPrincipal scripts
|
|
//
|
|
// contains code common to all the scripts
|
|
//
|
|
// Copyright (C) 1999 Microsoft Corporation.
|
|
|
|
|
|
|
|
' various manifest constants
|
|
const CLASS_USER = 0
|
|
const CLASS_LOCAL_GROUP = 1
|
|
const CLASS_GLOBAL_GROUP = 2
|
|
const CLASS_OTHER = 3
|
|
|
|
' the elements of this array are indexed by the above constants
|
|
dim classNames(2)
|
|
classNames(CLASS_USER) = "User"
|
|
classNames(CLASS_LOCAL_GROUP) = "Group"
|
|
classNames(CLASS_GLOBAL_GROUP) = "Group"
|
|
|
|
' from iads.h
|
|
const ADS_GROUP_TYPE_DOMAIN_LOCAL_GROUP = &H4
|
|
const ADS_GROUP_TYPE_GLOBAL_GROUP = &H2
|
|
const ADS_GROUP_TYPE_UNIVERSAL_GROUP = &H8
|
|
const ADS_GROUP_TYPE_SECURITY_ENABLED = &H80000000
|
|
const ADS_NAME_INITTYPE_DOMAIN = 1
|
|
const ADS_NAME_INITTYPE_SERVER = 2
|
|
const ADS_NAME_TYPE_1779 = 1
|
|
const ADS_NAME_TYPE_NT4 = 3
|
|
const ADS_NAME_TYPE_SID_OR_SID_HISTORY_NAME = 12
|
|
const ADS_PROPERTY_APPEND = 3
|
|
const ADS_PROPERTY_DELETE = 4
|
|
const ADS_PROPERTY_UPDATE = 2
|
|
|
|
' from lmaccess.h
|
|
const UF_TEMP_DUPLICATE_ACCOUNT = &H0100
|
|
const UF_NORMAL_ACCOUNT = &H0200
|
|
|
|
' from andyhar's adsi reskit
|
|
const ADS_SID_RAW = 0
|
|
const ADS_SID_HEXSTRING = 1
|
|
const ADS_SID_SDDL = 4
|
|
const ADS_SID_WINNT_PATH = 5
|
|
const ADS_SID_ACTIVE_DIRECTORY_PATH = 6
|
|
|
|
const E_ADS_UNKNOWN_OBJECT = &H80005004
|
|
const E_ADS_ERROR_DS_NO_SUCH_OBJECT = &H80072030
|
|
const E_ADS_ERROR_DS_NAME_NOT_FOUND = &H80072116
|
|
|
|
|
|
|
|
' create the COM object implementing ICloneSecurityPrincipal
|
|
dim clonepr
|
|
set clonepr = CreateObject("DSUtils.ClonePrincipal")
|
|
if Err.Number then DumpErrAndQuit
|
|
|
|
' create the COM object implementing IADsNameTranslate
|
|
dim nameTranslate
|
|
set nameTranslate = CreateObject("NameTranslate")
|
|
if Err.Number then DumpErrAndQuit
|
|
|
|
' create the COM object implementing IADsPathname
|
|
dim adsPathname
|
|
set adsPathname = CreateObject("Pathname")
|
|
if Err.Number then DumpErrAndQuit
|
|
|
|
' create the COM object implementing IADsError
|
|
dim adsError
|
|
set adsError = CreateObject("DSUtils.ADsError")
|
|
if Err.Number then DumpErrAndQuit
|
|
|
|
' create the COM object implementing IADsSID
|
|
dim sid
|
|
set sid = CreateObject("DSUtils.ADsSID")
|
|
if Err.Number then DumpErrAndQuit
|
|
|
|
|
|
|
|
'
|
|
' functions and subroutines follow
|
|
'
|
|
|
|
|
|
sub CloneSecurityPrincipal(byref srcObject, byval srcSam, byval dstDom, byval dstDC, byval dstSam, byval dstDN)
|
|
on error resume next
|
|
|
|
' verify that the source object is of a type that we support
|
|
dim srcObjectClass
|
|
srcObjectClass = ObjectClass(srcObject)
|
|
|
|
select case srcObjectClass
|
|
case CLASS_USER
|
|
if srcObject.UserFlags and UF_TEMP_DUPLICATE_ACCOUNT then
|
|
Echo "Source object is a temporary local user account, which is not supported."
|
|
wscript.quit(0)
|
|
end if
|
|
case CLASS_LOCAL_GROUP
|
|
case CLASS_GLOBAL_GROUP
|
|
' do nothing
|
|
case else
|
|
' not a supported object class
|
|
Echo "Source object is of type " & srcObject.Class & ", which is not supported by this tool."
|
|
wscript.quit(0)
|
|
end select
|
|
|
|
' bind to the destination object
|
|
|
|
' we attempt to locate the destination object by it's sam account name, in
|
|
' order to determine if that name is already in use by a security principal
|
|
' in the destination domain.
|
|
|
|
dim dstObjectSamPath
|
|
dstObjectSamPath = "WinNT://" & dstDom & "/" & dstDC & "/" & dstSam
|
|
|
|
dim dstObjectDNPath
|
|
dstObjectDNPath = "LDAP://" & dstDC & "/" & dstDN
|
|
|
|
dim dstObjectClass
|
|
dim dstObject
|
|
|
|
Err.Clear
|
|
set dstObject = GetObject(dstObjectSamPath)
|
|
dim errnum1
|
|
errnum1 = Err.Number
|
|
select case errnum1
|
|
case E_ADS_UNKNOWN_OBJECT
|
|
' destination is not found
|
|
|
|
Echo "Destination object " & dstSam & " not found (by SAM name) path used: " & dstObjectSamPath
|
|
|
|
' bind to the DN of the object, then
|
|
Err.Clear
|
|
set dstObject = GetObject(dstObjectDNPath)
|
|
dim errnum2
|
|
errnum2 = Err.Number
|
|
select case errnum2
|
|
case E_ADS_ERROR_DS_NO_SUCH_OBJECT
|
|
Echo "Destination object " & dstDN & " not found (by DN) path used: " & dstObjectDNPath
|
|
|
|
' create the dstDN object of the same type as the source
|
|
Err.Clear
|
|
set dstObject = CreateDestinationDN(dstSam, dstDN, dstDC, srcObjectClass)
|
|
|
|
case 0
|
|
' dstDN found
|
|
|
|
Echo "Destination DN found"
|
|
|
|
dstObjectClass = ObjectClass(dstObject)
|
|
|
|
if dstObjectClass <> srcObjectClass then
|
|
Bail "Source and destination objects differ in class type."
|
|
end if
|
|
|
|
if UCase(dstObject.SamAccountName) <> UCase(dstSam) then
|
|
' sam name of the object is not the same as the sam name
|
|
' specified on the command line
|
|
Bail "SAM account name of " & dstDN & " is " & dstObject.SamAccountName & " not " & dstSam
|
|
end if
|
|
|
|
case else
|
|
Echo "Error attempting to bind to " & dstObjectDNPath
|
|
DumpErrAndQuit
|
|
|
|
end select
|
|
|
|
case 0
|
|
' dstSam found. Find the DN of the object it refers to
|
|
|
|
Echo "Destination SAM name found"
|
|
|
|
nameTranslate.Init ADS_NAME_INITTYPE_SERVER, dstDC
|
|
if Err.Number then DumpErrAndQuit
|
|
|
|
nameTranslate.Set ADS_NAME_TYPE_NT4, dstDom & "\" & dstSam
|
|
if Err.Number then DumpErrAndQuit
|
|
|
|
dim foundDN
|
|
foundDN = nameTranslate.Get(ADS_NAME_TYPE_1779) ' aka full DN
|
|
if Err.Number then DumpErrAndQuit
|
|
|
|
Echo dstSam & " refers to " & foundDN
|
|
|
|
if UCase(dstDN) <> UCase(foundDN) then
|
|
' sam name is in use by another object than the one the user
|
|
' indicated.
|
|
Bail "SAM account name " & dstSam & " is in use by object " & foundDN & ", not " & dstDN
|
|
end if
|
|
|
|
' at this point, we've verified that the sam name specified by the
|
|
' user matches the DN. Now verify that the DN refers to an object
|
|
' of the same type as the source
|
|
|
|
set dstObject = GetObject("LDAP://" & dstDC & "/" & foundDN)
|
|
if Err.Number then DumpErrAndQuit
|
|
|
|
dstObjectClass = ObjectClass(dstObject)
|
|
if dstObjectClass <> srcObjectClass then
|
|
Bail "Source and destination objects differ in class type."
|
|
end if
|
|
|
|
case else
|
|
Echo "Error attempting to bind to destination object " & dstObjectSamPath
|
|
DumpErrAndQuit
|
|
end select
|
|
|
|
' at this point, dstObject is bound to the object onto which we
|
|
' should clone the source object
|
|
|
|
' copy the source object's properties
|
|
Echo "Setting properties for target " & dstObject.Class & " " & dstObject.Name
|
|
select case srcObjectClass
|
|
case CLASS_USER
|
|
|
|
' copy the properties of the source user to the destination user
|
|
clonepr.CopyDownlevelUserProperties srcSam, dstSam, 0
|
|
if Err.Number then DumpErrAndQuit
|
|
|
|
Echo "Downlevel properties set."
|
|
|
|
' fixup the destination user's group memberships
|
|
|
|
FixupUserGroupMemberships srcObject, dstObject, dstDC
|
|
if Err.Number then DumpErrAndQuit
|
|
|
|
Echo "User's Group memberships restored."
|
|
|
|
' commit the changes
|
|
dstObject.SetInfo
|
|
if Err.Number then DumpErrAndQuit
|
|
|
|
Echo "User changes commited."
|
|
|
|
case CLASS_LOCAL_GROUP
|
|
' copy the source group's description
|
|
if srcObject.Description <> "" then
|
|
dstObject.Put "Description", srcObject.Description
|
|
dstObject.SetInfo
|
|
if Err.Number then DumpErrAndQuit
|
|
end if
|
|
|
|
Echo "Local group description set."
|
|
|
|
' copy the source local group's membership
|
|
CopyLocalGroupMembership srcObject, dstObject
|
|
if Err.Number then DumpErrAndQuit
|
|
|
|
Echo "Local group membership copied."
|
|
|
|
' commit the changes
|
|
dstObject.SetInfo
|
|
if Err.Number then DumpErrAndQuit
|
|
|
|
Echo "Local group changes commited."
|
|
|
|
case CLASS_GLOBAL_GROUP
|
|
' copy the source group's description
|
|
if srcObject.Description <> "" then
|
|
dstObject.Put "Description", srcObject.Description
|
|
dstObject.SetInfo
|
|
if Err.Number then DumpErrAndQuit
|
|
end if
|
|
|
|
Echo "Global group description set."
|
|
|
|
' fixup the destination group's members
|
|
FixupGlobalGroupMembers srcObject, dstObject, dstDC
|
|
if Err.Number then DumpErrAndQuit
|
|
|
|
Echo "Global group memberships restored."
|
|
|
|
' commit the change
|
|
dstObject.SetInfo
|
|
if Err.Number then DumpErrAndQuit
|
|
|
|
Echo "Global group changes commited."
|
|
|
|
case else
|
|
' why are we here? what is my purpose in life?
|
|
wscript "illegal code path"
|
|
wscript.quit(0)
|
|
|
|
end select
|
|
|
|
' Add the SID of the source principal to the sid history of the destination
|
|
' principal.
|
|
Echo "Adding SID for source " & srcObject.Class & " " & srcObject.Name & " to SID history of target " & dstObject.Class & " " & dstObject.Name
|
|
clonepr.AddSidHistory srcSam, dstSam, 0
|
|
if Err.Number then DumpErrAndQuit
|
|
|
|
Echo "SID history set successfully."
|
|
|
|
' all done
|
|
Echo srcObject.Name & " cloned successfully."
|
|
end sub
|
|
|
|
|
|
|
|
' Create a DS security principal object, and return a bound reference to it.
|
|
'
|
|
' samName - in, sam account name of object-to-be
|
|
'
|
|
' DN - in, full DN of the object to be created
|
|
'
|
|
' DC - in, name of domain controller on which the object is to be created
|
|
'
|
|
' objectClass - in, CLASS_ constant for the type of object to create
|
|
|
|
function CreateDestinationDN(byval samName, byval DN, byval DC, byval objectClass)
|
|
on error resume next
|
|
Echo "Creating " & DN
|
|
|
|
' determine the name of the container to place the new object by removing
|
|
' the leaf-most portion of the DN
|
|
dim p
|
|
p = InStr(1, DN, ",", 1)
|
|
|
|
dim dstCN
|
|
dstCN = Mid(DN, 1, p - 1) ' - 1 to omit the comma
|
|
|
|
dim ouDN, ouDNPath
|
|
ouDN = Mid(DN, p + 1) ' + 1 to skip the comma
|
|
ouDNPath = "LDAP://" & DC & "/" & ouDN
|
|
|
|
dim container, errnum3
|
|
set container = GetObject(ouDNPath)
|
|
select case Err.Number
|
|
case E_ADS_ERROR_DS_NO_SUCH_OBJECT
|
|
Bail "Container " & ouDN & " not found"
|
|
case 0
|
|
' do nothing
|
|
case else
|
|
Echo "Error attempting to bind to " & ouDN
|
|
DumpErrAndQuit
|
|
end select
|
|
|
|
dim dstObject
|
|
set dstObject = container.Create(classNames(objectClass), dstCN)
|
|
if Err.Number then
|
|
Echo "Error attempting to create " & DN
|
|
DumpErrAndQuit
|
|
end if
|
|
|
|
dstObject.Put "samAccountName", samName
|
|
if Err.Number then
|
|
Echo "Error attempting to set samAccountName for " & DN
|
|
DumpErrAndQuit
|
|
end if
|
|
|
|
select case objectClass
|
|
case CLASS_USER
|
|
' nothing more to add
|
|
|
|
case CLASS_LOCAL_GROUP
|
|
' set group type to local
|
|
dstObject.Put "groupType", ADS_GROUP_TYPE_DOMAIN_LOCAL_GROUP + ADS_GROUP_TYPE_SECURITY_ENABLED
|
|
if Err.Number then
|
|
Echo "Error attempting to set local group type for " & DN
|
|
DumpErrAndQuit
|
|
end if
|
|
|
|
case CLASS_GLOBAL_GROUP
|
|
' set group type to global
|
|
dstObject.Put "groupType", ADS_GROUP_TYPE_GLOBAL_GROUP + ADS_GROUP_TYPE_SECURITY_ENABLED
|
|
if Err.Number then
|
|
Echo "Error attempting to set global group type for " & DN
|
|
DumpErrAndQuit
|
|
end if
|
|
|
|
end select
|
|
|
|
dstObject.SetInfo
|
|
if Err.Number then
|
|
Echo "Error attempting to commit create of " & DN
|
|
DumpErrAndQuit
|
|
end if
|
|
|
|
Echo "Created " & DN
|
|
|
|
set CreateDestinationDN = dstObject
|
|
end function
|
|
|
|
|
|
|
|
' for each group to which the source user object belongs, look for that
|
|
' group's sid in the sid histories of objects in the destination forest
|
|
' (domain?). If found, add the destination user as a member of the located
|
|
' group. Thus, when a user is cloned, the clone becomes a member of all the
|
|
' existing cloned groups corresponding to the original groups the
|
|
' orignal user belonged to.
|
|
|
|
sub FixupUserGroupMemberships(byref srcObject, byref dstObject, byval dstDC)
|
|
on error resume next
|
|
Echo "Fixing group memberships for " & dstObject.Class & " " & dstObject.Name
|
|
|
|
nameTranslate.Init ADS_NAME_INITTYPE_SERVER, dstDC
|
|
if Err.Number then DumpErrAndQuit
|
|
|
|
dim group
|
|
dim sidString
|
|
for each group in srcObject.Groups
|
|
if (ObjectClass(group) = CLASS_GLOBAL_GROUP) then
|
|
Echo " Found global group " & group.ADsPath
|
|
|
|
sid.SetAs ADS_SID_WINNT_PATH, group.AdsPath & "," & group.Class
|
|
if Err.Number then DumpErrAndQuit
|
|
|
|
sidString = sid.GetAs(ADS_SID_SDDL)
|
|
if Err.Number then DumpErrAndQuit
|
|
|
|
if IsBuiltInSid(sidString) then
|
|
Echo " " & group.ADsPath & " is a built-in group"
|
|
|
|
' built-ins are present in every domain with the same sid. So we
|
|
' can't search for the corresponding destination object by sid, or
|
|
' we may be multiple matches (if there is more than 1 domain in the
|
|
' destination forest, and the destination DC also happens to be
|
|
' a global catalog). So, here we compose a sid-style LDAP path
|
|
' for the built-in destination object.
|
|
|
|
sidString = "<sid=" & sid.GetAs(ADS_SID_HEXSTRING) & ">"
|
|
if Err.Number then DumpErrAndQuit
|
|
|
|
dim mypath
|
|
mypath = "LDAP://" & dstDC & "/" & sidString
|
|
|
|
dim mygroup
|
|
set mygroup = GetObject(mypath)
|
|
if Err.Number then DumpErrAndQuit
|
|
|
|
if not IsUserMemberOfGroup(mygroup, dstObject) then
|
|
Echo " Adding " & dstObject.Name & " to group " & mygroup.Name
|
|
mygroup.Add dstObject.AdsPath
|
|
else
|
|
Echo " " & dstObject.Name & " is already member of " & mygroup.Name
|
|
end if
|
|
if Err.Number then DumpErrAndQuit
|
|
else
|
|
|
|
' find the DN of the object with that sid as its object sid or in
|
|
' its sid history (the sid history is where it will be, if the object
|
|
' is a clone).
|
|
|
|
nameTranslate.Set ADS_NAME_TYPE_SID_OR_SID_HISTORY_NAME, sidString
|
|
select case Err.Number
|
|
case E_ADS_ERROR_DS_NAME_NOT_FOUND
|
|
' do nothing: skip this member; it hasn't been cloned yet
|
|
|
|
Echo " Skipping " & group.ADsPath & " -- not cloned yet"
|
|
|
|
case 0
|
|
' found!
|
|
dim foundDN
|
|
foundDN = ""
|
|
foundDN = nameTranslate.Get(ADS_NAME_TYPE_1779) ' aka full DN
|
|
|
|
select case Err.Number
|
|
case E_ADS_ERROR_DS_NAME_NOT_FOUND
|
|
' do nothing: skip this member; it hasn't been cloned yet
|
|
case 0
|
|
AddUserToGroup dstObject, foundDN, dstDC
|
|
case else
|
|
DumpErrAndQuit
|
|
end select
|
|
|
|
case else
|
|
DumpErrAndQuit
|
|
|
|
end select
|
|
end if
|
|
else
|
|
Echo " Skipping group " & group.AdsPath & " -- not global group"
|
|
end if
|
|
|
|
' need to clear this so next iteration won't choke.
|
|
Err.Clear
|
|
next
|
|
end sub
|
|
|
|
|
|
|
|
' for each member of the source local group, obtain the member's SID and add
|
|
' that SID as a member of the destination local group. If that SID does not
|
|
' refer to a security principal in the destination domain, then the SAM will
|
|
' create a Foreign Principal Object (FPO) to represent that SID. then SAM
|
|
' will replace the reference to the SID in the group membership with the DN
|
|
' of the FPO. An FPO acts like a proxy for the SID.
|
|
|
|
sub CopyLocalGroupMembership(byref srcObject, byref dstObject)
|
|
on error resume next
|
|
|
|
Echo "Copying local group membership"
|
|
|
|
' get the sids in string form of each of the members of the source
|
|
' group. collect them in an array
|
|
dim member
|
|
dim sidString
|
|
dim sidStringArray()
|
|
dim i
|
|
i = 0
|
|
|
|
dim dn
|
|
dn = dstObject.Get("distinguishedName")
|
|
if Err.Number then DumpErrAndQuit
|
|
|
|
Echo " Getting destination group membership as SIDs"
|
|
|
|
dim dstExistingMemberSIDs
|
|
dstExistingMemberSIDs = clonepr.GetMembersSIDs(dn)
|
|
if Err.Number then DumpErrAndQuit
|
|
|
|
dim numExistingMembers
|
|
numExistingMembers = 0
|
|
dim x
|
|
for each x in dstExistingMemberSIDs
|
|
numExistingMembers = numExistingMembers + 1
|
|
next
|
|
|
|
for each member in srcObject.Members
|
|
dim sidDeletedAccount
|
|
if IsDeletedAccount(member.AdsPath, sidDeletedAccount) then
|
|
Echo " Considering deleted account: " & sidDeletedAccount
|
|
sid.SetAs ADS_SID_SDDL, sidDeletedAccount
|
|
else
|
|
Echo " Considering normal account: " & member.AdsPath
|
|
sid.SetAs ADS_SID_WINNT_PATH, member.AdsPath & "," & member.Class
|
|
end if
|
|
if Err.Number then DumpErrAndQuit
|
|
|
|
sidString = "<sid=" & sid.GetAs(ADS_SID_HEXSTRING) & ">"
|
|
if Err.Number then DumpErrAndQuit
|
|
|
|
if (0 = numExistingMembers) Or (not SidStringExists(sidString, dstExistingMemberSIDs)) then
|
|
Echo " Adding " & sidString
|
|
redim preserve sidStringArray(i)
|
|
sidStringArray(i) = sidString
|
|
i = i + 1
|
|
end if
|
|
next
|
|
|
|
' use the array to update the destination group in one whack.
|
|
if i then
|
|
if 0 = numExistingMembers then
|
|
dstObject.PutEx ADS_PROPERTY_UPDATE, "member", sidStringArray
|
|
else
|
|
dstObject.PutEx ADS_PROPERTY_APPEND, "member", sidStringArray
|
|
end if
|
|
if Err.Number then DumpErrAndQuit
|
|
|
|
dstObject.SetInfo
|
|
if Err.Number then DumpErrAndQuit
|
|
end if
|
|
end sub
|
|
|
|
|
|
|
|
function IsDeletedAccount(byref AdsPath, byref sidDeletedAccount)
|
|
dim pos0, pos1
|
|
pos0 = InStr(1, AdsPath, "://", 1)
|
|
pos1 = InStr(pos0 + 3, AdsPath, "/", 1)
|
|
|
|
if 0 = pos1 then
|
|
IsDeletedAccount = True
|
|
sidDeletedAccount = Mid(AdsPath, pos0 + 3)
|
|
else
|
|
IsDeletedAccount = False
|
|
end if
|
|
|
|
end function
|
|
|
|
|
|
|
|
function SidStringExists(byref sidString, byref dstExistingMemberSIDs)
|
|
dim sid
|
|
sid = UCase(sidString)
|
|
|
|
SidStringExists = False
|
|
|
|
dim x
|
|
For each x in dstExistingMemberSIDs
|
|
if UCase(x) = sid then
|
|
Echo " Skipping existing sid " & x
|
|
SidStringExists = True
|
|
exit function
|
|
end if
|
|
next
|
|
|
|
end function
|
|
|
|
|
|
|
|
' for each member of the source global group, look for that member's sid in
|
|
' the sid histories of objects the destination forest (domain?). If found,
|
|
' add that located object as a member of the destination group. Thus,
|
|
' when a global group is cloned, the existing clones of all users that belong
|
|
' to the original group will belong to the cloned group.
|
|
|
|
sub FixupGlobalGroupMembers(byref srcObject, byref dstObject, byval dstDC)
|
|
on error resume next
|
|
Echo "Fixing group membership for " & dstObject.Class & " " & dstObject.Name
|
|
|
|
nameTranslate.Init ADS_NAME_INITTYPE_SERVER, dstDC
|
|
if Err.Number then DumpErrAndQuit
|
|
|
|
dim member
|
|
dim sidString
|
|
for each member in srcObject.Members
|
|
|
|
if member.UserFlags and UF_NORMAL_ACCOUNT then
|
|
|
|
' extract the sid of the account
|
|
sid.SetAs ADS_SID_WINNT_PATH, member.AdsPath & "," & member.Class
|
|
if Err.Number then DumpErrAndQuit
|
|
|
|
sidString = sid.GetAs(ADS_SID_SDDL)
|
|
if Err.Number then DumpErrAndQuit
|
|
|
|
' find the DN of the member with that sid as its object sid or in
|
|
' its sid history (the sid history is where it will be, if the member
|
|
' is a clone).
|
|
nameTranslate.Set ADS_NAME_TYPE_SID_OR_SID_HISTORY_NAME, sidString
|
|
select case Err.Number
|
|
case E_ADS_ERROR_DS_NAME_NOT_FOUND
|
|
' do nothing: skip this member; it hasn't been cloned yet
|
|
|
|
case 0
|
|
' found!
|
|
dim foundDN
|
|
foundDN = ""
|
|
foundDN = nameTranslate.Get(ADS_NAME_TYPE_1779) ' aka full DN
|
|
|
|
select case Err.Number
|
|
case E_ADS_ERROR_DS_NAME_NOT_FOUND
|
|
' do nothing: skip this member; it hasn't been cloned yet
|
|
case 0
|
|
' add the dn to the members property of the dst object
|
|
dim path
|
|
path = "LDAP://" & dstDC & "/" & foundDN
|
|
Dim tempObj
|
|
set tempObj = GetObject(path)
|
|
if Err.Number then DumpErrAndQuit
|
|
if NOT IsUserMemberOfGroup( dstObject, tempObj ) then
|
|
Echo " adding " & foundDN & " to group " & dstObject.Name
|
|
dstObject.Add path
|
|
end if
|
|
if Err.Number then DumpErrAndQuit
|
|
|
|
case else
|
|
DumpErrAndQuit
|
|
end select
|
|
|
|
case else
|
|
DumpErrAndQuit
|
|
end select
|
|
|
|
' need to clear this so the next iteration doesn't choke
|
|
Err.Clear
|
|
|
|
else
|
|
|
|
' skip computer, temp and trust accounts
|
|
Echo " Skipping non-user account " & member.Name
|
|
end if
|
|
next
|
|
end sub
|
|
|
|
|
|
|
|
' user - in, reference to user object, bound with LDAP provider.
|
|
'
|
|
' groupDN - in, full DN of the group to which the user is to be added
|
|
'
|
|
' dstDC - in, name of destination domain controller
|
|
|
|
sub AddUserToGroup(byref user, byval groupDN, byval dstDC)
|
|
on error resume next
|
|
|
|
dim path
|
|
path = "LDAP://" & dstDC & "/" & groupDN
|
|
|
|
dim group
|
|
set group = GetObject(path)
|
|
if Err.Number then DumpErrAndQuit
|
|
|
|
if not IsUserMemberOfGroup(group,user) then
|
|
Echo " Adding " & user.Name & " to group " & group.Name
|
|
group.Add user.AdsPath
|
|
else
|
|
Echo " " & user.Name & " is already member of " & group.Name
|
|
end if
|
|
if Err.Number then DumpErrAndQuit
|
|
end sub
|
|
|
|
|
|
|
|
function IsUserMemberOfGroup( byref group, byref user )
|
|
if group.IsMember(user.AdsPath) then
|
|
IsUserMemberOfGroup = True
|
|
exit function
|
|
end if
|
|
|
|
sid.SetAs ADS_SID_ACTIVE_DIRECTORY_PATH, group.AdsPath
|
|
if Err.Number then DumpErrAndQuit
|
|
|
|
dim sidString
|
|
sidString = sid.GetAs(ADS_SID_SDDL)
|
|
if Err.Number then DumpErrAndQuit
|
|
if Len(sidString) > 9 then
|
|
dim lastDash
|
|
lastDash = InStrRev(sidString, "-", -1, 1)
|
|
if lastDash then
|
|
dim ridString
|
|
ridString = Mid(sidString, lastDash + 1)
|
|
if StrComp(ridString,user.PrimaryGroupId,1) = 0 then
|
|
IsUserMemberOfGroup = True
|
|
exit function
|
|
end if
|
|
end if
|
|
end if
|
|
|
|
IsUserMemberOfGroup = False
|
|
end function
|
|
|
|
|
|
|
|
' based on the class of the object, return one of CLASS_USER,
|
|
' CLASS_LOCAL_GROUP, CLASS_GLOBAL_GROUP, CLASS_OTHER
|
|
|
|
function ObjectClass(object)
|
|
dim cls
|
|
cls = UCase(object.Class)
|
|
|
|
if cls = "GROUP" then
|
|
if (object.GroupType and ADS_GROUP_TYPE_DOMAIN_LOCAL_GROUP) then
|
|
' type is local group
|
|
ObjectClass = CLASS_LOCAL_GROUP
|
|
exit function
|
|
else
|
|
if ((object.GroupType and ADS_GROUP_TYPE_GLOBAL_GROUP) or (object.GroupType and ADS_GROUP_TYPE_UNIVERSAL_GROUP)) then
|
|
' type is global group
|
|
ObjectClass = CLASS_GLOBAL_GROUP
|
|
exit function
|
|
end if
|
|
end if
|
|
else
|
|
if cls = "USER" then
|
|
' type is user
|
|
ObjectClass = CLASS_USER
|
|
exit function
|
|
end if
|
|
end if
|
|
|
|
' type is not recognized
|
|
ObjectClass = CLASS_OTHER
|
|
exit function
|
|
end function
|
|
|
|
|
|
|
|
' returns non-zero if the stringized SID refers to a well-known rid, zero
|
|
' otherwise
|
|
|
|
function HasWellKnownRid(byval sidString)
|
|
' a SID refers to a well-known account if the first sub-authority (aka
|
|
' RID) is < 1000. The first subauthority is the last portion of the
|
|
' stringized SID
|
|
|
|
if Len(sidString) > 9 then
|
|
dim lastDash
|
|
lastDash = InStrRev(sidString, "-", -1, 1)
|
|
if lastDash then
|
|
dim ridString
|
|
ridString = Mid(sidString, lastDash + 1)
|
|
if CLng(ridString) < 1000 then
|
|
HasWellKnownRid = True
|
|
exit function
|
|
end if
|
|
end if
|
|
end if
|
|
|
|
HasWellKnownRid = False
|
|
end function
|
|
|
|
|
|
|
|
' returns non-zero if the stringized SID refers to a builtin sid, zero
|
|
' otherwise
|
|
|
|
function IsBuiltInSid(byval sidString)
|
|
' a SID refers to builtin account or group if it has prefix S-1-5-32-
|
|
|
|
if Len(sidString) > 9 then
|
|
dim prefixString
|
|
prefixString = Mid(sidString, 1, 9)
|
|
if StrComp( prefixString, "S-1-5-32-", 1 ) = 0 then
|
|
IsBuiltInSid = true
|
|
exit function
|
|
end if
|
|
end if
|
|
|
|
IsBuiltInSid = False
|
|
end function
|
|
|
|
|
|
|
|
' searches for and returns the value of a command line argument of the form
|
|
' /argName:value from the supplied array. erases the entry in the array so
|
|
' that only untouched entries remain.
|
|
|
|
function GetArgValue(argName, args())
|
|
dim a
|
|
dim v
|
|
dim argNameLength
|
|
dim x
|
|
dim argCount
|
|
dim fullArgName
|
|
|
|
fullArgName = "/" & argName & ":"
|
|
argCount = Ubound(args)
|
|
|
|
' Get the length of the argname we are looking for
|
|
argNameLength = Len(fullArgName)
|
|
GetArgValue = "" ' default to nothing
|
|
|
|
for x = 0 To argCount
|
|
if Len(args(x)) >= argNameLength then
|
|
|
|
a = Mid(args(x), 1, argNameLength)
|
|
if UCase(a) = UCase(fullArgName) then
|
|
|
|
' erase it so we can look for unknown args later
|
|
v = args(x)
|
|
args(x) = ""
|
|
|
|
if Len(v) > argNameLength then
|
|
GetArgValue = Mid(v, argNameLength + 1)
|
|
exit function
|
|
else
|
|
GetArgValue = ""
|
|
exit function
|
|
end if
|
|
end if
|
|
end if
|
|
next
|
|
end function
|
|
|
|
|
|
|
|
' walks thru the array searching for any non-empty element. if at least one
|
|
' is found, then return non-zero. Otherwise return 0.
|
|
|
|
function CheckForBadArgs(byref args())
|
|
dim i
|
|
for i = 0 to UBound(args)
|
|
if Len(args(i)) > 0 then
|
|
CheckForBadArgs = 1
|
|
exit function
|
|
end if
|
|
next
|
|
|
|
CheckForBadArgs = 0
|
|
end function
|
|
|
|
|
|
|
|
sub DumpErrAndQuit
|
|
dim errnum
|
|
errnum = Err.Number
|
|
|
|
Echo "Error 0x" & CStr(Hex(errnum)) & " occurred."
|
|
if len(Err.Description) then
|
|
Echo "Error Description: " & Err.Description
|
|
end if
|
|
if len(Err.Source) then
|
|
Echo "Error Source : " & Err.Source
|
|
end if
|
|
Echo "ADsError Description: "
|
|
Echo adsError.GetErrorMsg(errnum)
|
|
wscript.quit(0)
|
|
end sub
|
|
|
|
|
|
|
|
sub Bail(byref message)
|
|
Echo "Error: " & message
|
|
wscript.quit(0)
|
|
end sub
|
|
|
|
|
|
|
|
sub Echo(byref message)
|
|
wscript.echo message
|
|
end sub
|
|
|
|
|
|
|
|
' clonepr.vbi end
|
|
|
|
|
|
|
|
|