Creating an AD Shell
Hello Again
This is my third post in the Active Directory and PowerShell series. In this post I want to talk about how to create your own pseudo-cmdlets to manage the AD. I`m going to talk about how to present scripts to PowerShell that behave in a similar way to Cmdlets, that then allow easy administration of the AD. You may well think I`m mad, but how cool would it be to do this in a shell:
PS C:\> get-dn benp 'LDAP://CN=benp,OU=Admins,DC=umpadom,DC=com' PS C:\> enable-user benp User: benp enabled PS C:\> disable-user benp User: benp disabled PS C:\> Set-ADProp benp GivenName Ben GivenName property set for user benp PS C:\> Get-ADProp benp GivenName Ben |
Wait a minute! There is no native support for Active Directory that does this. What magic is this!
Well in this post I`m going to show you how to create these example pseudo-cmdlets, and show you that whilst they are mighty cool, there is nothing that difficult or clever about them.
Creating the Pseudo-Cmdlets
First thing I want to be clear about is that these commands are not real Cmdlets. They have not been added via a snapin, and do not behave in the same way as Cmdlets. The command “get-dn” is simply a function that calls a script, and passes in some arguments. The script then does some stuff and returns some text. It does not return an object! Cmdlets normally return objects – these don’t. However, we can use them to create a shell like interface to do common tasks.
The Script
Let’s take Enable-User as an example. Probably best you read my previous 2 posts before you look at this, might make it a bit easier to understand:
# Function to enable user # Takes SAM Name $Error.clear() $ErrorActionPreference = "silentlycontinue" function get-dn ($SAMName) { $root = [ADSI]'' $searcher = new-object System.DirectoryServices.DirectorySearcher($root) $searcher.filter = ("(sAMAccountName= $SAMName)") $user = $searcher.findall() if ($user.count -gt 1) { $count = 0 foreach($i in $user) { write-host $count ": " $i.path $count = $count + 1 } $selection = Read-Host "Please select item: " return $user[$selection].path } else { return $user[0].path } } $userdn = get-dn $Args[0] $user = [ADSI] $userdn $user.psbase.invokeset("AccountDisabled", "False") $user.setinfo() if ($error.count -ne 0) { "Error enabling user: " + $Args[0] $error[0].exception } else { "User: " + $Args[0] + " enabled" } |
Most of this code is the same as get-dn, which I explained in my previous post. There only difference is what we do to the AD, and a bit of basic error checking. Let’s go through what happens in the code once we have the path to the user object we want to manipulate:
$userdn = get-dn $Args[0] $user = [ADSI] $userdn $user.psbase.invokeset("AccountDisabled", "False") $user.setinfo()
|
All we do is create an object with the user object path as a constructor. Once we have this we simply set the user property AccountDisabled to be False. Then we commit the change to the AD. Simple! I use the invokeset method here, because it’s really easy to use. If you use the ADSI provider its actually a bit harder than the .Net Framework method.
The error handling here is very basic. I might write a post on error handling soon. The basic algorithm for error handling is:
Clear the $error.count
Do Stuff
If the $error.count is greater than 0 (we had an error)
Output anerror message
Else
Output operation successful text
Turning the script to a Pseudo-Cmdlet
To make the script accessible from just typing “enable-user”, all we need to do is create a function that utilises that script. To create a function we use the new-item Cmdlet. Here’s the code I use:
new-item -path function:enable-user -value {& 'C:\scripts\enable-user.ps1' $args[0]}
|
There we go. An easy way to enable user accounts.
That function will be loaded in PowerShell until the instance of PowerShell closes. So how can we make the function persistent? Well there are 2 ways. The simple way is to add this line to your profile. The profile file can be found by typing $profile in the shell. That means that every time you load PowerShell the function will be loaded.
However, I don’t really want that. I want to be able to load a script with all these functions when I want to administer the AD. Therefore I collected all my function definitions into 1 file: ADSupport.ps1. This file contains this:
new-item -path function:get-dn -value {& 'C:\scripts\getuserdn.ps1' $args[0]} new-item -path function:get-adprop -value {& 'C:\scripts\get-adprop.ps1' $args[0] $args[1]} new-item -path function:set-adprop -value {& 'C:\scripts\set-adprop.ps1' $args[0] $args[1] $args[2]} new-item -path function:disable-user -value {& 'C:\scripts\disable-user.ps1' $args[0]} new-item -path function:enable-user -value {& 'C:\scripts\enable-user.ps1' $args[0]}
|
Problem: if I run .\adsupport.ps1 it does not import these functions. If you try it you’ll find that none the functions are available. This is because by default the script is executed in a different scope. We want it executed in the PowerShell scope and therefore we must use an extra full stop (period if you live across the atlantic).
. .\adsupport.ps1 |
Now all your functions will be available to use.
Other Scripts
Here is the code to get all the above functions working.
Get-DN
See previous post
Disable User
# Function to disable user # Takes SAM Name $error.psbase.clear() $erroractionpreference = "silentlycontinue" function get-dn ($SAMName) { $root = [ADSI]'' $searcher = new-object System.DirectoryServices.DirectorySearcher($root) $searcher.filter = ("(sAMAccountName= $SAMName)") $user = $searcher.findall() if ($user.count -gt 1) { $count = 0 foreach($i in $user) { write-host $count ": " $i.path $count = $count + 1 } $selection = Read-Host "Please select item: " return $user[$selection].path } else { return $user[0].path } } $userdn = get-dn $Args[0] $user = [ADSI] $userdn $user.psbase.invokeset("AccountDisabled", "True") $user.setinfo() if ($error.count -ne 0) { "Error disabling user: " + $Args[0] $error[0].exception } else { "User: " + $Args[0] + " disabled" } |
Get-ADProp
# Function to query AD Object Properties # Takes SAM, Property $Error.psbase.clear() $ErrorActionPreference = "SilentlyContinue" #Use the below line in the profile to assign to get-adprop #new-item -path function:get-adprop -value {& 'C:\store\ps scripts\get-adprop.ps1' $args[0] $args[1]} function get-dn ($SAMName) { $root = [ADSI]'' $searcher = new-object System.DirectoryServices.DirectorySearcher($root) $searcher.filter = ("(sAMAccountName= $SAMName)") $user = $searcher.findall() if ($user.count -gt 1) { $count = 0 foreach($i in $user) { write-host $count ": " $i.path $count = $count + 1 } $selection = Read-Host "Please select item: " return $user[$selection].path } else { return $user[0].path } } $username = get-dn $Args[0] $user = [ADSI] $username $user.Get($Args[1]) if ($error.count -ne 0) { "Error getting property " + $Args[1] + " for " + $Args[0] $error[0].exception } |
Set-ADProp
# Function to set AD Object Properties # Takes SAM, Property, Value #Use the below line in the profile to assign to get-adprop #new-item -path function:set-adprop -value {& 'C:\store\ps scripts\set-adprop.ps1' $args[0] $args[1] $args[2]} function get-dn ($SAMName) { $root = [ADSI]'' $searcher = new-object System.DirectoryServices.DirectorySearcher($root) $searcher.filter = ("(sAMAccountName= $SAMName)") $user = $searcher.findall() if ($user.count -gt 1) { $count = 0 foreach($i in $user) { write-host $count ": " $i.path $count = $count + 1 } $selection = Read-Host "Please select item: " return $user[$selection].path } else { return $user[0].path } } $username = get-dn $Args[0] $user = [ADSI] $username $user.put($Args[1], $Args[2]) $user.setinfo() if ($error.count -ne 0) { "Error setting " + $Args[1] $error[0].exception } else { $Args[1] + " property set for user" + $Args[0] } |
Well ladies and gents. Hope you enjoyed this. Coding errors on a post card please J
That is all
BenP
Comments
Anonymous
January 01, 2003
Ben you are the dogs whatits. This is just an awesome post on how to 1) Make ad admin simpler and 2)Anonymous
January 01, 2003
Ladies + Gents This week I have had the privilege of working at TechEd 2007. I’ve been on the PowerShellAnonymous
July 17, 2007
Wouldn't it just be much easier to place the functions in a directory and the simply add that to the path? In this way, you do not have to double the function/files. I have a subdir called library being part of my path, so I simply stick files in to that when I want them to be easy accessible. Per