Getting Smart with Logon Scripts
I’m sure anyone who administers an Active Directory infrastructure will have had some experience working with logon scripts. Logon scripts can either be deployed on a user by user basis or, more commonly, through Group Policy. Logon scripts can be written in the scripting language of your choice, but my preference is to use vbscript with the Active Directory Services Interface (ADSI). Using this powerful combination in your logon scripts gives you the ability to configure almost anything with regard to your user’s desktop environment, from drive and printer mapping, shortcut distribution, registry settings and collecting inventory information.
However, powerful logon scripts which have the ability to do this are often a double edged sword. They can end up containing a lot of code and if they are modified incorrectly then they have the potential to negatively impact a lot of users, who will usually get error messages displayed on screen and suffer lost productivity if the syntax of the code is wrong, not to mention the heat that the administrator who made the mistake will suffer.
In this article I want to share a method which allows you to use vbscripts and ADSI for your logon scripts and at the same time minimise the code stored in them and minimise the need for scripts to be edited, which should mean that the potential to get it wrong should be greatly reduced.
In most organisations the typical things that logon scripts do, such as mapping drives, connecting to printers and writing registry entries are executed if the user in question belongs to a particular security group in Active Directory. Typically this can lead to logon script code which enumerates a user’s group membership and then executes a number of conditional statements, either If-Then-Else loops or Case Select statements. This is where the code tends to get unwieldy, as you need a conditional statement for each group membership. In the large enterprise you can have hundreds of security groups that require action leading to a logon script length of 1000 lines or more. Also, every time a new group comes online you will have to add to the code in the logon script, introducing the potential for error.
My recommended approach is to remove the actions associated with group membership from the script altogether and place it in the Active Directory group object instead. This leads to:
- A logon script which requires infrequent change
- A relatively short script length
As with all AD objects a security group has many fields which typically tend not to be populated and can be used for other purposes. I tend to use the Notes field as it isn’t used often. I place code in this field which I want the logon script to execute. This means that the majority of code editing takes place here and if an error is made it only has the potential to affect members of that group rather than the entire organisation.
The following sample script is a good example of this in action:
'Example script only, to be used in a production environment at your own risk
On Error Resume Next
'Create script Objects
Set adSys = CreateObject("ADSystemInfo")
set wscr = CreateObject("WScript.Shell")
Set oNet = WScript.CreateObject("Wscript.Network")
'Get name of domain controller providing authentication
Set oDomain = getObject("LDAP://rootDse")
sDC = oDomain.Get("dnsHostName")
'Get Computer Name
sComp = oNet.ComputerName
'Get logon name of User
sSamAc = oNet.username
‘Get Distinguished name of User
sCompName = adsys.computername
sUserName = adsys.username
sPrefix = "LDAP://"
'Bind to user object in AD
Set oUser = GetObject(sPrefix & sUsername)
'Process security group Notes (info) field
' Create Array of users groups
Select Case VarType(oUser.MemberOf)
Case 0 'Empty
sMemberOfList = ""
Case 8 'one item
sMemberOfList = oUser.MemberOf
Case 8204 'Array
sMemberOfList = Join(oUser.memberof,";")
End Select
sMemberOfList = LCase(sMemberOfList)
aGroups = Split(sMemberOfList,";")
' Map drives, copy icons, etc according to group membership
For i = LBound(aGroups) To UBound(aGroups)
sGroupName = aGroups(i)
Set oGroup = GetObject(sPrefix + sGroupName)
If not oGroup.Info = "" Then
aCmds = Split(oGroup.Info,";")
For j = LBound(aCmds) To UBound(aCmds)
sCmd = aCmds(j)
Execute sCmd
Next
End If
Next
'Script finished
'Clear Memory
Set oDrives = Nothing
Set oUser = Nothing
Set oDomain = Nothing
Set fso = Nothing
'Set oCommand = Nothing
'Set oCon = Nothing
Set oNet = Nothing
Set Wscr = Nothing
Set adsys = Nothing
WScript.Quit
Sub MapDrive(DriveLetter,ConnectDPath)
' If the drive letter supplied is not an asterisk, use letter supplied
If DriveLetter <> "*" Then
oNet.MapNetworkDrive DriveLetter, ConnectDPath, False
'Network drive already mapped
If Err.Number = -2147024811 Then
Err.Clear
oNet.RemoveNetworkDrive DriveLetter, True, True
oNet.MapNetworkDrive DriveLetter, ConnectDPath, False
End If
Else
' Drive letter supplied was an asterisk, so assign a letter based on what is free
' Enumerate current drive mappings
Set oDrives = oNet.EnumNetworkDrives
' Create a list of drive letters in use
sDriveList = ""
For k = 0 to oDrives.Count - 1 Step 2
sDriveList = sDriveList & "," & oDrives.Item(k)
Next
Set oDrives = Nothing
' Start at Z (chr(90)) and count down to I (chr(73)) and map first drive letter not used
' Amended to go from P (chr(80)) instead
For k = 80 To 73 Step -1
' Check if the letter you want to use is in the currently used list
If InStr(1, sDriveList, Chr(k)) = 0 Then
' If not, map the drive
oNet.MapNetworkDrive Chr(k) & ":", ConnectDPath, False
' Now that drive is mapped, can exit loop
Exit For
End If
' If you get to here, and k=73, all letters must have been used so drive could not be mapped
' This error number would occur is you tried to map a drive that was already in use, so raise it
If k = 73 Then Err.Raise(-2147024811)
Next
End If
End Sub
Sub RunCmd(sCommand)
wscr.run "cmd /C " & sCommand,0,true
End Sub
This example script does the following:
- Sets On Error Resume Next, meaning that if a processing error is encountered the script will not crash but will continue to run without displaying errors to the user.
- Creates a number of objects for use by the code
- Gets the logon account of the user, along with the computer account name and the distinguished names of both the user and the computer
- Binds to the user object in Active Directory and creates an array of the user’s group membership.
- For each group that the user is a member of look in the Notes field. If code is found there, execute it.
- Quit
The script has 2 subroutines. The RunCmd subroutine is called to execute each piece of code in the group notes field from within the For-Next loop which enumerates each piece of code. The MapDrive subroutine is called from within the code in the notes field to map a network drive.
So, if the code in the notes field is in the format to map a network drive, connect to a printer distribute a shortcut and write a registry entry, all of these actions take place, due to the fact that the user is in that particular group. Each command in the notes field is separated by a semicolon, so the script will parse the notes field using semicolons to create a number of distinct actions. Let’s take a look at an example group notes field:
In this example the MapDrive subroutine will be called twice to map the users G: drive and L: drive to the relevant share and a key will be added to the registry.
One important point about this method is that you need to protect the Notes field of group objects so that only designated trusted administrators can modify the content of the field. This can easily be accomplished by delegating permissions through Active Directory.
So there you have it, a simple yet powerful method of deploying logon scripts in your Active Directory infrastructure which gives you all the benefits of vbscript and ADSI and minimises the downsides. You can find a wealth of information on scripting against Active Directory and other Microsoft Technologies at the following link:
https://www.microsoft.com/technet/scriptcenter/default.mspx
Disclaimer
The sample scripts are not supported under any Microsoft standard support program or service. The sample scripts are provided AS IS without warranty of any kind. Microsoft further disclaims all implied warranties including, without limitation, any implied warranties of merchantability or of fitness for a particular purpose. The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. In no event shall Microsoft, its authors, or anyone else involved in the creation, production, or delivery of the scripts be liable for any damages whatsoever (including, without limitation, damages for loss of business profits, business interruption, loss of business information, or other pecuniary loss) arising out of the use of or inability to use the sample scripts or documentation, even if Microsoft has been advised of the possibility of such damages.
Comments
Anonymous
January 01, 2003
I love this script, however I'm trying to make 2 registry settings created based on group membership, however I can only get one of the registry writes to work. Here's my code that is in the notes of the Group: wscr.RegWrite "HKCUSoftwareODBCODBC.inisybase_madLoginID","MAD_A","REG_SZ",wscr.RegWrite "HKCUSoftwareODBCODBC.inisybase_madPassword","password","REG_SZ" I've never tried using the notes field before and I've tried all kinds of syntax variations of the above script. Any help is greatly appreciated. Thanks in advance.Anonymous
January 01, 2003
Got it working, I guess I didn't pay enough attention to detail. "Each command in the notes field is separated by a semicolon..." Thanks again!