Logon scripts serve a critical function in a network. You can use them to provide a consistent environment for users. They should be carefully designed to run reliably for all of the target users in all situations. Logon scripts should be tested to make sure they perform as expected for everyone, not just administrators.

Of course logon scripts should be coded carefully to avoid errors. No one wants users to get error messages during logon. However, I would recommend not using "On Error Resume Next" in logon scripts, except to trap errors you anticipate might be raised by specific statements, after which normal error handling is restored with "On Error GoTo 0". If something unexpected happens in a logon script, you want the user to be alerted so that the problem can be addressed. If all error messages are suppressed, it can be nearly impossible to troubleshoot problems. In fact, it is possible that no one will know there is a problem in the logon script, even though some vital function failed.

I recommend the following to make your logon scripts easier to troubleshoot, understand, and maintain:

  1. Do not suppress normal error handling with "On Error Resume Next", except where needed.
  2. Use "Option Explicit" and declare all variables in "Dim" statements.
  3. Make liberal use of comments to document what the script is doing.
  4. Use blank lines and indenting to make the logic of the script clear.
  5. Use the LDAP provider rather than the WinNT provider.
  6. Do not hard code credentials or elevate privileges.
  7. Avoid tasks that should be done once, or find a way for the script to avoid repeating the task.
  8. Avoid tasks that will take a long time or tempt the user to interrupt the program.
  9. Be aware that many people may run the logon script simultaneously.

Performance is always a consideration in logon scripts. Other than launching other programs, like setup programs, usually the only statements that slow down a script are ones that bind to objects in Active Directory. Binding to local objects, like the wshNetwork object or a dictionary object, is very fast. Binding to a few Active Directory objects is no problem, but binding to many group objects, for example, should be avoided.

A very common task in logon scripts is mapping network drives and printers. The script can ensure that all users get standard mappings appropriate for their situation. Often mappings are dependent on group membership. If it is only necessary to check membership in a few groups, the easiest method is to use the IsMember method of the group object. This requires binding to the group object. An example:

Option Explicit
Dim objNetwork, objSysInfo, strUserDN, strAdsPath, objGroup

Set objNetwork = CreateObject("Wscript.Network")

' Retrieve user DN.
Set objSysInfo = CreateObject("ADSystemInfo")
strUserDN = objSysInfo.UserName
strAdsPath = "LDAP://" & strUserDN

' Bind to group object.
Set objGroup = GetObject("LDAP://TestGroup,ou=Sales,dc=MyDomain,dc=com")

' Test for membership in the group.
If (objGroup.IsMember(strAdsPath) = True) Then
    ' Map a drive.
    objNetwork.MapNetworkDrive "K:", "\\MyServer\GroupShare"
End If

If you need to check membership in more than a few groups, it would be more efficient to enumerate all of the groups the user is a member of and keep track of them in a dictionary object. If you are not concerned about membership due to group nesting, you can use the Groups method of the group object. For example:

Option Explicit
Dim objNetwork, objSysInfo, strUserDN, objUser, objList, objGroup

Set objNetwork = CreateObject("Wscript.Network")

' Retrieve user DN.
Set objSysInfo = CreateObject("ADSystemInfo")
strUserDN = objSysInfo.UserName

' Bind to the user object.
Set objUser = GetObject("LDAP://" & strUserDN)

' Setup dictionary object to track group memberships.
Set objList = CreateObject("Scripting.Dictionary")
objList.CompareMode = vbTextCompare

' Enumerate groups and populate the dictionary object.
For Each objGroup In objUser.Groups
    objList.Add objGroup.sAMAccountName, True
Next

' Check group membership.
If (objList.Exists("TestGroup") = True) Then
    ' Map a drive.
    objNetwork.MapNetworkDrive "K:", "\\MyServer\GroupShare"
End If

You can test for membership in as many groups as desired without slowing down the script. Also, you can use the NT name of the group or the Distinguished Name in your checks.

The Groups method of the user object returns a collection of group objects. Although you do not use a Set statement to bind to each group, the method must bind to all of the groups, so this can be slow if the user is a member of many groups. A better method would be to enumerate the memberOf attribute of the user. This multi-valued attribute is a collection of the Distinguished Names of all groups the user is a direct member of (except the "primary" group of the user). However, you must account for the three possible situations: that there are no groups in the memberOf collection, one group, or more than one. For example:

Option Explicit
Dim objNetwork, objSysInfo, strUserDN, objUser, objList
Dim arrstrGroups, strGroupDN

Set objNetwork = CreateObject("Wscript.Network")

' Retrieve user DN.
Set objSysInfo = CreateObject("ADSystemInfo")
strUserDN = objSysInfo.UserName

' Bind to the user object.
Set objUser = GetObject("LDAP://" & strUserDN)

' Setup dictionary object to track group memberships.
Set objList = CreateObject("Scripting.Dictionary")
objList.CompareMode = vbTextCompare

' Enumerate groups and populate the dictionary object.
arrstrGroups = objUser.memberOf
If (IsEmpty(arrstrGroups) = False) Then
    If (TypeName(arrstrGroups) = "String") Then
        objList.Add arrstrGroups, True
    Else
        For Each strGroupDN In arrstrGroups
            objList.Add strGroupDN, True
        Next
    End If
End If

' Check group membership.
If (objList.Exists("cn=TestGroup,ou=West,dc=MyDomain,dc=com") = True) Then
    ' Map a drive.
    objNetwork.MapNetworkDrive "K:", "\\MyServer\GroupShare"
End If

This is faster if the user is a member of more than a few groups, but you can only retrieve the full Distinguished Names of the groups. This is a small sacrifice for speed.

If you need to account for membership due to group nesting, a bit more work is required. Many examples are on this web site, but the simplest method is a recursive subroutine to enumerate all user group memberships. This subroutine would populate a dictionary object so the enumeration is done just once. Also, the dictionary object allows you to avoid an infinite loop if the group nesting is circular. An example:

Option Explicit
Dim objNetwork, objSysInfo, strUserDN, objUser, objList

Set objNetwork = CreateObject("Wscript.Network")

' Retrieve user DN.
Set objSysInfo = CreateObject("ADSystemInfo")
strUserDN = objSysInfo.UserName

' Bind to the user object.
Set objUser = GetObject("LDAP://" & strUserDN)

' Setup dictionary object to track group memberships.
Set objList = CreateObject("Scripting.Dictionary")
objList.CompareMode = vbTextCompare

' Enumerate groups and populate the dictionary object.
Call EnumGroups(objUser)

' Check group membership.
If (objList.Exists("TestGroup") = True) Then
    ' Map a drive.
    objNetwork.MapNetworkDrive "K:", "\\MyServer\GroupShare"
End If

Sub EnumGroups(ByVal objADObject)
    ' Recursive subroutine to enumerate user group memberships.
    ' The object reference objList must have global scope.
    Dim objGroup

    For Each objGroup In objADObject.Groups
        If (objList.Exists(objGroup.sAMAccountName) = False) Then
            objList.Add objGroup.sAMAccountName, True
            Call EnumGroups(objGroup)
        End If
    Next
End Sub

Again, once the dictionary object is populated, you can check for membership in as many groups as desired with almost no affect on performance. However, the script must bind to all groups the user is a member of. This is necessary to expand the group nesting.

In all of the examples above the MapNetworkDrive method of the wshNetwork object is used to map drives. This method can raise an error if there is a persistent mapping that conflicts. There are several ways to handle this, but I like to use a subroutine that traps the error if the mapping fails, then removes any existing mapping for the drive letter and tries again. For example:

Sub MapDrive(ByVal strDrive, ByVal strShare)
    ' Subroutine to map network share to a drive letter.
    ' The object reference objNetwork must have global scope.

    ' Trap error if drive cannot be mapped.
    On Error Resume Next
    objNetwork.MapNetworkDrive strDrive, strShare
    If (Err.Number <> 0) Then
        ' Attempt to remove existing drive mapping.
        objNetwork.RemoveNetworkDrive strDrive, True, True
        If (Err.Number <> 0) Then
            Call MsgBox("Unable to map drive " & strDrive _
                & " to " & strShare & "." _
                & vbCrLf & "Error number: " & Err.Number _
                & vbCrLf & "Description: " & Err.Description, _
                vbOKOnly + vbCritical, "Logon Script")
            Exit Sub
        End If
        ' Restore normal error handling.
        On Error GoTo 0
        ' Attempt to map drive to share again.
        objNetwork.MapNetworkDrive strDrive, strShare
    End If
End Sub

This subroutine would be used in the logon script similar to below:

' Check group membership.
If (objList.Exists("TestGroup") = True) Then
    ' Map a drive.
    Call MapDrive("K:", "\\MyServer\GroupShare")
End If