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:

  1. 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.
  2. Creates a number of objects for use by the code
  3. Gets the logon account of the user, along with the computer account name and the distinguished names of both the user and the computer
  4. Binds to the user object in Active Directory and creates an array of the user’s group membership.
  5. For each group that the user is a member of look in the Notes field. If code is found there, execute it.
  6. 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.