Deprovision and UnDeprovision Users and Groups with PowerShell

Since I’ve got deprovisioning problems on the brain for some reason, here are a few little things that do work as expected.

Using Quest’s Active Directory Management Shell, connected to ARS (Connect-QADService -Proxy) as a user allowed to deprovision objects, Deprovision-QADUser works like a champ on any user who could be deprovisioned via the ARS MMC or the web interface. However, there is no UnDeprovision-QADUser or UndoDeprovision-QADUser or even Deprovision-QADUser -Undo.

There are also no cmdlets that parallel the Deprovision/Undo Deprovision functions available for groups in the ARS MMC and web interface.

The edsvaDeprovisionType and edsvaUnDeprovision attributes are accessible via the -ObjectSettings parameter of the Set-QADUser and Set-QADGroup cmdlets. So, here’s what I’ve added to my profile to remedy these slight shortcomings of the otherwise amazing QADMS:


# Makes deprovisioning groups by script similar to deprovisioning users.

function Deprovision-QADGroup {
Param($Group)

Get-QADGroup -Identity $Group | Set-QADGroup -ObjectAttributes @{edsvaDeprovisionType=1}

}

# For undeprovisioning groups. Would work the same for undeprovisioning users.

function Undeprovision-QADGroup {
Param($Group)

Get-QADGroup -Identity $Group | Set-QADGroup -ObjectAttributes @{edsvaUnDeprovision=1}

}

# Undeprovision user.

function Undeprovision-QADUser {
Param($User)

Get-QADUser -Identity $User | Set-QADUser -ObjectAttributes @{edsvaUnDeprovision=1}

}

Before you get carried away, remember that what Deprovision-QADGroup actually does is as much up to you or your ARS developer/consultant as the effects of Deprovision-QADUser are; likewise, you have to do a bit of work for what Undo Deprovision does.

Advertisement

Getting ints out of Exchange 2010 disk space statistics Strings when remoting

July 2015 update: This still applies to Exchange 2013 – I’ve not tested it yet with Exchange 2016, though…

I’m back, this time with Exchange stuff. It’s been awhile, mostly due to my company’s IT department having found out what happens when DNS and AD do not get along and everything starts falling back to NTLM authentication, as well as a long Thanksgiving vacation back to the States. However, I’ve begun a project to overhaul our old user provisioning process, and ActiveRoles Server is at the center of it.

User provisioning means email provisioning, and in an Exchange environment, that means Exchange cmdlets.

The PowerShell cmdlets for Exchange 2010 are about 90% awesome. Why not 100%? Little stuff like returning disk space statistics from Get-MailboxDatabase -Status or Get-MailboxStatistics as strings when doing implicit remoting, not the ByteQuantifiedSize type returned when you use Exchange Management Shell on the server itself, or as a simple int.

This is because ByteQuantifiedSize is not a regular .NET type, but a Microsoft.Exchange.Data type, so instead of rendering it as a plain-vanilla int when the Microsoft.Exchange.Management.PowerShell.E2010 snap-in isn’t loaded, PowerShell renders it as a String. Microsoft wants us to use implicit remoting instead of the snap-in when running remotely. Make up your minds, Exchange team!

Technet forum discussion about this inconsistency: Exchange Server 2010: why string here and int there with EMC?

Example:

Get-MailboxDatabase -Server US1234 -Status `
| Select-Object AvailableNewMailboxSpace `
| Sort-Object -Descending


AvailableNewMailboxSpace
------------------------
94.88 MB (99,483,648 bytes)
1.58 GB (1,696,464,896 bytes)

Wait… 1.58 GB is bigger than 94.88 MB, right? Not if they’re strings.

However, this can be overcome. I’m posting this because all the ways I could find are stuck deep inside rather long scripts. This method is from http://poshcode.org/1902, by Karl Mitschke (see line 120). I chose to leave it as bytes rather than converting to MB or GB, as the destination for this data is a SQL Server table that will immediately be used for the user provisioning project, but might be used for other things in the future.

Our mailbox databases are named for the sites they’re at: for example, one of the ones at the US-Dallas site would be US-Dallas-MD01. The sitename can be extracted with Split(‘-‘).

Get-MailboxDatabase -Status `
| Select-Object @{Name="Sitename"; `
Expression={$_.Name.split('-')[0]+'-'+$_.Name.split('-')[1]}}, `
Name, Server, `
@{Name="Available"; `
Expression={[int]($_.availablenewmailboxspace.split("(")[1].Split()[0])}} `
| Sort-Object -Property @{Expression="Sitename";Descending=$false}, `
@{Expression="Available";Descending=$true}

That finally got me what I was looking for.

Here’s a function for converting those disk space strings to integers – I’ve added it to the script I use to start Exchange implicit remoting sessions from my workstation.

Function Get-IntFromExchangeNumberString {
Param($NumberString)
 [int]($NumberString.split("(")[1].Split()[0])
}

If there is a better and/or more concise way to do any of what I’ve done, feel free to put it in the comments or post a link!

Fun Fact: EDMS:// is to ARS as LDAP:// (or GC://) is to Active Directory

If your organization is anything like mine, you would have a rebellion on your hands if you said that you were taking native AD privileges away, and that any old scripts that relied on ADSI wouldn’t work anymore, but, hey, look – PowerShell awesomeness in the Quest Active Directory Management Shell!

Yeah. Don’t do that. There’s no reason to – ARS has an ADSI provider that works enough like the native AD one that it can be used pretty much the same way.

Here’s a peace offering to accompany the announcement that native AD privileges will be a thing of the past: they can simply do a find on LDAP://whateverserver and replace it with EDMS://yourARSloadbalancer. If their scripts have it in the form of LDAP://distinguishednameonly, it’s simply a matter of replacing LDAP:// with EDMS://. Same with GC:// (uses a global catalog server instead of any old domain controller like LDAP:// will) – just replace it with EDMS://.

I can’t guarantee that this will work 100% of the time, but my colleagues who have tried it report that it has for them, and they are heartily grateful that I’m not making them learn PowerShell yet.

Make sure the script is run on a machine with the ADSI provider for the correct version of ARS installed. Which is installed when you install the Quest AD Management Shell. Which they might later discover is awesome for quick things they used to write nasty VBScripts for.

Related Fun Fact: if you know the DN of an object, referring to it by [adsi]"EDMS://distinguishedname" is usually WAY faster than Get-QADObject -Identity distinguishedname. In my environment, from the interactive shell in PowerGUI, the ADSI way consistently takes less than 0.1 ms, while the nifty cmdlet way takes about 150 ms (YIKES!). The convenience of QAD cmdlets has its price. For command-line one-offs, though, they’re a whole lot of wonderful.

A Mystery Solved with ActiveRoles Change History

This post is a tutorial on both how to look at ActiveRoles change History and User Activity in Quest Active Directory Management Shell (QADMS) and using PowerShell to discover and extract data hidden in objects. Get-QARSOperation is the cmdlet at the heart of this.

Users are mysteriously disappearing from Active Directory, and people are casting suspicious glances your way. Whodunnit?

Fortunately for you, ActiveRoles Server keeps track of that sort of thing, as long as you’ve got Change History turned on. User object deletion is something that ARS tracks by default.

When using an initial, exploratory “Get-” in QADMS, I recommend setting a low return set size limit; here, I’ve used 5. That’s enough to pick up varied results to get an idea of what data is available, but low enough to be quick.

Get-QARSOperation -OperationType Delete -TargetObjectType User -SizeLimit 5


ID InitiatedOn InitiatedBy Status Type Target
-- ----------- ----------- ------ ---- ------

(Loads of info I don’t feel like sanitizing, so I’m just leaving it off)

Sweet!

Get-QARSOperation -OperationType delete -TargetObjectType User -SizeLimit 5 | Select-Object InitiatedBy, Target


InitiatedBy Target
----------- ------

 

(aaaand… nothing.)

Try again, this time with Format-List:

Get-QARSOperation -OperationType delete -TargetObjectType User | Format-List *


Controls : {13, AllowApproval}
ID : 1-734056
OperationGuid : 87c6ac52-07ec-43e8-abb7-1f7050ae7918
Type : Delete
Status : Completed
Initiated : 10/4/2012 12:22:30 PM
Completed : 10/4/2012 12:22:30 PM
InitiatorInfo : Quest.ActiveRoles.ArsPowerShellSnapIn.BusinessLogic.ManagementHistory.PrincipalInformationImpl
TargetObjectInfo : Quest.ActiveRoles.ArsPowerShellSnapIn.BusinessLogic.ManagementHistory.ObjectInformationImpl
TasksCount : 0

This tells us that the InitiatorInfo and TargetObjectInfo is really inside some objects. What’s in those objects?

Get-QARSOperation -OperationType delete -TargetObjectType User -SizeLimit 5 | foreach { $_.initiatorinfo | fl *; $_.targetobjectinfo | fl * }


Host : <<some pc name>>
Site : <<AD Site pc is in>>
IsDSAdmin : False
DN : <<the distinguished name of the guilty party>>
Guid : <<their GUID>>
Sid : <<their SID>>
NTAccountName : <<their NT account name in domain\username format>>
ObjectClass : user

DN : <<the distinguished name of the victim>>
Guid : <<their GUID>>
Sid : <<their SID>>
NTAccountName : (blank – ActiveRoles Server does not save this by default)
ObjectClass : user

Ok, now we’re getting somewhere. Use some calculated properties with Select-Object to pull the parts out that you want, making new PSObjects with Perp, Victim and Date properties

Get-QARSOperation -OperationType Delete -TargetObjectType User | Select-Object @{ name="Perp"; expression= {$_.InitiatorInfo.NTAccountName }}, @{ name="Victim"; expression={ $_.TargetObjectInfo.DN }}, @{ name="Date"; expression={ $_.Initiated }}

You can then write this to a CSV for future reference and Excel viewing by piping it into Export-CSV:

Get-QARSOperation -OperationType Delete -TargetObjectType User | Select-Object @{ name="Perp"; expression= {$_.InitiatorInfo.NTAccountName }}, @{ name="Victim"; expression={ $_.TargetObjectInfo.DN }}, @{ name="Date"; expression={ $_.Initiated }} | Export-CSV -Path "C:\Logs\UserDeletes-2012Oct04.csv" -NoTypeInformation

Mystery solved.