Table of Contents

Active Directory client management, join/unjoin/relocate ou

created by LarsG lars.gruenheid@civitec.de 2015/09/30

With this package, you can join or leave a domain, and in theory change the ou-path for the client within a domain (still experimental, details below). These three functions are conveniently assigned to the action requests setup (join), uninstall (unjoin), update (relocate).

This package relies on three product properties:

domain_ou must follows this syntax: [domain.tld][/ou_1/ou_2], both segments are independent and optional and only taken into account when joining a domain or relocating to another ou. when a client shall leave a domain, required information are gathered from operating system.

if no domain is specified, it's being extracted from host identifier. if no ou is specified, the client will be placed in the default computer ou-path for the domain. each ou needs a leading forward-slash, all have to be in the right order, beginning at the top-most level.

username must include the domain it belongs to, either like DOMAIN\username or username@domain.tld, and it has to be an account with sufficient privileges to join/unjoin clients to/from the domain(s) you want to manage.

username and password are prunned from productproperties upon every successful execution, so that they won't remain for everyone to see in cleartext. i hope an option for password-masking in productproperties will be available soon.

Setup

if a client currently is in a domain and shall join another, this script will try to unjoin from the current domain, and then joined to the new domain, with the same administrative account you provided. so you will need one account with sufficient privileges for both domains, f.e. a trusted management domain containing such administrative accounts. otherwise, you have to do both steps seperately - first unjoin, then join, with different accounts.

[Actions]
noUpdateScript
 
defVar $DomainRaw$
defVar $Domain$
defVar $DomainCurrent$
defVar $OUPath$
defVar $OUPathCurrent$
defVar $Username$
defVar $Password$
defVar $ExitCode$
defVar $JoinMode$
defVar $UnJoinMode$
defVar $JOIN_DOMAIN$
defVar $ACCT_CREATE$
defVar $ACCT_DELETE$
defVar $WIN9X_UPGRADE$
defVar $DOMAIN_JOIN_IF_JOINED$
defVar $JOIN_UNSECURE$
defVar $MACHINE_PASSWORD_PASSED$
defVar $DEFERRED_SPN_SET$
defVar $INSTALL_INVOCATION$
;defVar $ACCT_NO_OPTIONS$
;defVar $ACCT_DEACTIVATE$
 
;calculate joinmode from possible constants
set $JOIN_DOMAIN$ = "1"
set $ACCT_CREATE$ = "2"
set $ACCT_DELETE$ = "4"
set $WIN9X_UPGRADE$ = "16"
set $DOMAIN_JOIN_IF_JOINED$ = "32"
set $JOIN_UNSECURE$ = "64"
set $MACHINE_PASSWORD_PASSED$ = "128"
set $DEFERRED_SPN_SET$ = "256"
set $INSTALL_INVOCATION$ = "262144"
set $JoinMode$ = calculate($JOIN_DOMAIN$+"+"+$ACCT_CREATE$)
 
;calculate unjoinmode from possible constants
;set $ACCT_NO_OPTIONS$ = "0"
;set $ACCT_DEACTIVATE$ = "2"
;set $UnJoinMode$ = calculate($ACCT_DEACTIVATE$)
 
;product property domain_ou has the following syntax: [domain.tld][/ou_1/ou_2] - both segments are optional.
;domain must be the fqdn for the domain to join
;ou-path must consist of the hierarchical list of ou's the client should be put in, every ou must have a leading slash. 
set $DomainRaw$ = getProductProperty("domain_ou","")
 
;get domain from product property - if domain is not set, get domain from host identifier.
set $Domain$ = takeString(0, splitString($DomainRaw$,"/"))
if ($Domain$ = "")
	set $Domain$ = composeString(getSubList(1 : ,splitString("%HostId%",".")),".")
	set $DomainRaw$ = $Domain$ + $DomainRaw$
endif
 
;get ou-path from product property - if ou-path is not set, join client to standard client-ou-path for the domain.
;ou-path will be transformed into ldap-friendly syntax.
set $OUPath$ = ""
for %OU% in getSubList(1 : ,splitString($DomainRaw$,"/")) do set $OUPath$ = "OU=%OU%,"+$OUPath$
if not ($OUPath$ = "")
	for %DC% in splitString($Domain$,".") do set $OUPath$ = $OUPath$+"DC=%DC%,"
	set $OUPath$ = strPart($OUPath$,"1",calculate(strLength($OUPath$)+"-1"))
endif
 
;get username + password from, product properties, hide password value in log
set $Username$  = getProductProperty("username","")
setConfidential getProductProperty("password","")
set $Password$  = getProductProperty("password","")
 
;check if client is in domain 
if ( takeString(0, getOutStreamFromSection('execwith_vbs_check_domain cscript //nologo //e:vbs')) = "0" )
	showBitmap "%ScriptPath%\domain.png" "Active Directory" 
	message "Join domain " + $DomainRaw$
	execwith_vbs_domain_join cscript //nologo //e:vbs
	sub_check_domain_join
	dosinanicon_gpupdate /WaitOnClose
	comment "Setting default logon domain, clear previous login user"
	registry_default_domain /sysnative
	exitwindows /Reboot
else
	set $DomainCurrent$ = takeString(0, getOutStreamFromSection('execwith_vbs_get_domain cscript //nologo //e:vbs'))
	;check if client is already in domain requested
	if not ( $DomainCurrent$ = $Domain$ ) 
		showBitmap "%ScriptPath%\domain.png" "Active Directory" 
		message "Leave domain " + $DomainCurrent$
		execwith_vbs_domain_unjoin cscript //nologo //e:vbs
		sub_check_domain_unjoin
		;TODO remove client from ad
		;TODO check exitcode adsi
		exitwindows /ImmediateReboot
	else
		comment "Client is already in requested domain"
	endif
endif
 
;reset username + password entries in product properties
opsiservicecall_unset_username
opsiservicecall_unset_password
 
[execwith_vbs_check_domain]
Set obj = GetObject("WinMgmts:\\.\root\cimv2:Win32_ComputerSystem.Name='%PCNAME%'")
Wscript.Echo obj.PartOfDomain
 
[execwith_vbs_get_domain]
Set obj = GetObject("WinMgmts:\\.\root\cimv2:Win32_ComputerSystem.Name='%PCNAME%'")
Wscript.Echo obj.Domain
 
[execwith_vbs_domain_join]
Set obj = GetObject("WinMgmts:\\.\root\cimv2:Win32_ComputerSystem.Name='%PCNAME%'")
res = obj.JoinDomainOrWorkGroup("$Domain$", "$Password$", "$Username$", "$OUPath$", $JoinMode$)
Wscript.Quit res
 
[execwith_vbs_domain_unjoin]
Set obj = GetObject("WinMgmts:\\.\root\cimv2:Win32_ComputerSystem.Name='%PCNAME%'")
res = obj.UnjoinDomainOrWorkgroup("$Password$", "$Username$")
;res = obj.UnjoinDomainOrWorkgroup("$Password$", "$Username$", $UnJoinMode$)
Wscript.Quit res
 
[dosinanicon_gpupdate]
gpupdate /force
exit %ERRORLEVEL%
 
[registry_default_domain]
openkey [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon]
set "DefaultDomainName"    = REG_SZ:"$Domain$"
set "AltDefaultDomainName" = REG_SZ:"$Domain$"
set "CachePrimaryDomain"   = REG_SZ:"$Domain$"
openKey [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\LogonUI]
set "LastLoggedOnSAMUser" = REG_SZ:""
set "LastLoggedOnUser"    = REG_SZ:""
 
[opsiservicecall_unset_username]
"method": "setProductProperty"
"params": [
	"%installingProdName%",
	"username",
	"",
	"%hostid%"
]
 
[opsiservicecall_unset_password]
"method": "setProductProperty"
"params": [
	"%installingProdName%",
	"password",
	"",
	"%hostid%"
]
 
[sub_check_domain_unjoin]
set $ExitCode$ = getLastExitCode
if $ExitCode$ = "0" 
	comment "Success"
else
	logError "UNKNOWN ERROR " & ReturnValue
	isFatalError
endif
 
[sub_check_domain_join]
set $ExitCode$ = getLastExitCode
if $ExitCode$ = "0" 
	comment "Success"
else
if $ExitCode$ = "2" 
	logError "Missing OU"
	isFatalError
else
if $ExitCode$ = "5"
	logError "Access denied"
	isFatalError
else
if $ExitCode$ = "53" 
	logError "Network path not found"
	isFatalError
else
if $ExitCode$ = "87"
	logError "Parameter incorrect"
	isFatalError
else
if $ExitCode$ = "1326" 
	logError "Logon failure, user or pass"
	isFatalError
else
if $ExitCode$ = "1355" 
	logError "Domain can not be contacted"
	isFatalError
else
if $ExitCode$ = "1909" 
	logError "User account locked out"
	isFatalError
else
if $ExitCode$ = "2224" 
	logError "Computer Account allready exists"
	isFatalError
else
if $ExitCode$ = "2691" 
	logError "Allready joined"
	isFatalError
else
	logError "Unknown error " & ReturnValue
	isFatalError
endif
endif
endif
endif
endif
endif
endif
endif
endif
endif

Uninstall

It seems that actually deleting computer accounts from a domain upon unjoin is currently not possible, so keep in mind that you need to manually delete the account if you want it to be gone, f.e. to re-use the name for another computer. I am planning to add this as an optional feature.

[Actions]
defVar $DomainCurrent$
defVar $Username$
defVar $Password$
defVar $ExitCode$
defVar $UnJoinMode$
;defVar $ACCT_NO_OPTION$
;defVar $ACCT_DEACTIVATE$
 
;calculate unjoinmode from possible constants
;set $ACCT_NO_OPTION$ = "0"
;set $ACCT_DEACTIVATE$ = "2"
;set $UnJoinMode$ = calculate($ACCT_DEACTIVATE$)
 
;get username + password from, product properties, hide password value in log
set $Username$  = getProductProperty("username","")
setConfidential getProductProperty("password","")
set $Password$  = getProductProperty("password","")
 
;reset username + password entries in product properties
;opsiservicecall_unset_username
;opsiservicecall_unset_password
 
;check if client is in domain 
if not ( takeString(0, getOutStreamFromSection('execwith_vbs_check_domain cscript //nologo //e:vbs')) = "0" )
	set $DomainCurrent$ = takeString(0, getOutStreamFromSection('execwith_vbs_get_domain cscript //nologo //e:vbs'))
	showBitmap "%ScriptPath%\domain.png" "Active Directory" 
	message "Leave domain " + $DomainCurrent$
	;unjoin domain
	execwith_vbs_domain_unjoin cscript //nologo //e:vbs
	sub_check_domain_unjoin
	;TODO remove client from ad
	;TODO check exitcode adsi
	;restart client, script will be re-executed upon next opsi request 
	exitwindows /Reboot
else
	comment "Computer is currently not part of a domain"
endif
 
;reset username + password entries in product properties
opsiservicecall_unset_username
opsiservicecall_unset_password
 
[execwith_vbs_check_domain]
Set obj = GetObject("WinMgmts:\\.\root\cimv2:Win32_ComputerSystem.Name='%PCNAME%'")
Wscript.Echo obj.PartOfDomain
 
[execwith_vbs_get_domain]
Set obj = GetObject("WinMgmts:\\.\root\cimv2:Win32_ComputerSystem.Name='%PCNAME%'")
Wscript.Echo obj.Domain
 
[execwith_vbs_domain_unjoin]
Set obj = GetObject("WinMgmts:\\.\root\cimv2:Win32_ComputerSystem.Name='%PCNAME%'")
res = obj.UnjoinDomainOrWorkgroup("$Password$", "$Username$")
;res = obj.UnjoinDomainOrWorkgroup("$Password$", "$Username$", $UnJoinMode$)
Wscript.Quit res
 
[opsiservicecall_unset_username]
"method": "setProductProperty"
"params": [
	"%installingProdName%",
	"username",
	"",
	"%hostid%"
]
 
[opsiservicecall_unset_password]
"method": "setProductProperty"
"params": [
	"%installingProdName%",
	"password",
	"",
	"%hostid%"
]
 
[sub_check_domain_unjoin]
set $ExitCode$ = getLastExitCode
if $ExitCode$ = "0"
	comment "Success"
else
	logError "UNKNOWN ERROR " & ReturnValue
	isFatalError
endif

Update

Relocating a client to a different ou within the same domain is still giving me some headache, i am currently stuck at the part where the ADSI movehere function actually performs the relocation, it will throw an error Active Directory: not implemented (what ever that means).

If anyone can get this to work, i wouldn't mind a heads up (;

[Actions]
defVar $DomainRaw$
defVar $Domain$
defVar $DomainCurrent$
defVar $OUPath$
defVar $OUPathCurrent$
defVar $Username$
defVar $Password$
defVar $ExitCode$
 
;product property domain_ou has the following syntax: [domain.tld][/ou_1/ou_2] - both segments are optional.
;domain must be the fqdn for the domain to join
;ou-path must consist of the hierarchical list of ou's the client should be put in, every ou must have a leading slash. 
set $DomainRaw$ = getProductProperty("domain_ou","")
 
;get domain from product property - if domain is not set, get domain from host identifier.
set $Domain$ = takeString(0, splitString($DomainRaw$,"/"))
if ($Domain$ = "")
	set $Domain$ = composeString(getSubList(1 : ,splitString("%HostId%",".")),".")
	set $DomainRaw$ = $Domain$ + $DomainRaw$
endif
 
;get ou-path from product property - if ou-path is not set, join client to standard client-ou-path for the domain.
;ou-path will be transformed into ldap-friendly syntax.
set $OUPath$ = ""
for %OU% in getSubList(1 : ,splitString($DomainRaw$,"/")) do set $OUPath$ = "OU=%OU%,"+$OUPath$
if not ($OUPath$ = "")
	for %DC% in splitString($Domain$,".") do set $OUPath$ = $OUPath$+"DC=%DC%,"
	set $OUPath$ = strPart($OUPath$,"1",calculate(strLength($OUPath$)+"-1"))
endif
 
;get username + password from, product properties, hide password value in log
set $Username$  = getProductProperty("username","")
setConfidential getProductProperty("password","")
set $Password$  = getProductProperty("password","")
 
if not ( takeString(0, getOutStreamFromSection('execwith_vbs_check_domain cscript //nologo //e:vbs')) = "0" )
	set $DomainCurrent$ = takeString(0, getOutStreamFromSection('execwith_vbs_get_domain cscript //nologo //e:vbs'))
	;check if client is in domain requested
	if ( $DomainCurrent$ = $Domain$ ) 
		set $OUPathCurrent$ = takeString(0, getOutStreamFromSection('execwith_vbs_get_oupath cscript //nologo //e:vbs'))
		;check if client is already in ou-path requested
		if not ( $OUPathCurrent$ = $OUPath$ )
			showBitmap "%ScriptPath%\domain.png" "Active Directory" 
			message "Relocate to ou-path " + $DomainRaw$
			execwith_vbs_move_oupath cscript //nologo //e:vbs
			;TODO check exitcode adsi
			dosinanicon_gpupdate /WaitOnClose
		else
			comment "Computer is already in the given ou-path"
		endif
	else
		logError "Computer is not part of the domain requested, moving to another ou-path only works within the same domain"
		isFatalError
	endif
else
	logError "Computer is currently not part of a domain"
	isFatalError
endif
 
[execwith_vbs_check_domain]
Set obj = GetObject("WinMgmts:\\.\root\cimv2:Win32_ComputerSystem.Name='%PCNAME%'")
Wscript.Echo obj.PartOfDomain
 
[execwith_vbs_get_domain]
Set obj = GetObject("WinMgmts:\\.\root\cimv2:Win32_ComputerSystem.Name='%PCNAME%'")
Wscript.Echo obj.Domain
 
[execwith_vbs_get_oupath]
Set adoConnection = CreateObject("ADODB.Connection")
adoConnection.Provider = "ADsDSOObject"
adoConnection.Properties("User ID") = "$Username$"
adoConnection.Properties("Password") = "$Password$"
adoConnection.Properties("Encrypt Password") = True
Set adoCommand = CreateObject("ADODB.Command")
adoConnection.Properties("ADSI Flag") = &H200 Or &H1
adoConnection.Open "Active Directory Provider"
Set adoCommand.ActiveConnection = adoConnection
adoCommand.CommandText = "Select distinguishedName from 'LDAP://$Domain$' where objectClass='computer' and cn='%PCNAME%'"
adoCommand.Properties("Searchscope") = 2
Set adoRecordSet = adoCommand.Execute
adoRecordSet.MoveFirst
OUPathCurrent = adoRecordSet.Fields("distinguishedName").Value
Wscript.Echo Mid(OUPathCurrent,InStr(OUPathCurrent,",")+1)
 
[execwith_vbs_move_oupath]
Set obj = GetObject("LDAP:")
obj.OpenDSObject "LDAP://w8vrsag08.$Domain$/$OUPath$", "$Username$", "$Password$", 1
res = obj.MoveHere("LDAP://CN=%PCNAME%,$OUPathCurrent$", "CN=%PCNAME%")
Wscript.Quit res
 
[dosinanicon_gpupdate]
gpupdate /force
exit %ERRORLEVEL%
 
[opsiservicecall_unset_username]
"method": "setProductProperty"
"params": [
	"%installingProdName%",
	"username",
	"",
	"%hostid%"
]
 
[opsiservicecall_unset_password]
"method": "setProductProperty"
"params": [
	"%installingProdName%",
	"password",
	"",
	"%hostid%"
]