====== Active Directory client management, join/unjoin/relocate ou ====== created by LarsG [[lars.gruenheid@civitec.de]] 2015/09/30 * tested under windows 7 * tested under win2012 active directory environment * tested under winst 4.11.5.14 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** ''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** * **password** ''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%" ]