May 2012 - Posts

I do bit shifting in PowerShell all the time. It's quite necessary when you are working with option values in Active Directory, WMI, and various Win32 interfaces. .NET tends to use enumerations, which are tad easier to deal with in PowerShell.

I had never felt it necessary to break my routines out before, but I saw a posting on this topic by another individual today and I just thought he was making it harder than it had to be. I wanted to present another option to the community for these types of routines. I'm not putting down his work in any way - it was quite ingenious. I never would've thought of doing it that way.

Just FYI, 'shr' is an abbreviation for 'shift right' and correspondingly 'shl' is an abbreviation for 'shift left'. When I was in college (in the stone age) those function names were used in "Pascal extension libraries" and in "Fortran libraries" for performing shift operations. Shift operations are built-in as native operations to many (most?) compiled languages such as C and its various dialects.

Enjoy.

###
### Bit-shift operations
### bit-shift.ps1
###
### for bytes and short integers
###
### Michael B. Smith
### michael at TheEssentialExchange.com
### May, 2012
###

$bitsPerByte = 8	## [byte]
$bitsperWord = 16	## [int16] or [uint16]

function shift-left( [int]$valuesize, [int]$mask, $val, [int]$bits )
{
	if( $bits -ge $valuesize )
	{
		return 0
	}
	if( $bits -eq 0 )
	{
		return $val
	}
	if( $bits -lt 0 )
	{
		write-error "Can't shift by a negative value of bits"
		return -1
	}

	### it's possible to write this so that you never
	### overshift and generate an overflow. it's easier
	### to use a larger variable and mask at the end.

	[int]$result = $val
	for( $i = 0; $i -lt $bits; $i++ )
	{
		$result *= 2
	}

	return ( $result -band $mask )
}

function shift-right( [int]$valuesize, [int]$mask, $val, [int]$bits )
{
	if( $bits -ge $valuesize )
	{
		return 0
	}
	if( $bits -eq 0 )
	{
		return $val
	}
	if( $bits -lt 0 )
	{
		write-error "Can't shift by a negative value of bits"
		return -1
	}

	for( $i = 0; $i -lt $bits; $i++ )
	{
		## normally PowerShell does banker's rounding (well, .NET does)
		## we have to override that here to get true integer division.
		$val = [Math]::Floor( $val / 2 )
	}

	return $val
}

function shl-byte( [byte]$val, [int]$bits )
{
	$result = shift-left $bitsPerByte 0xff $val $bits
	if( $result -lt 0 )
	{
		return $result
	}

	return ( [byte]$result )
}

function shr-byte( [byte]$val, [int]$bits )
{
	$result = shift-right $bitsPerByte 0xff $val $bits
	if( $result -lt 0 )
	{
		return $result
	}

	return ( [byte]$result )
}

function shl-word( [uint16]$val, [int]$bits )
{
	$result = shift-left $bitsPerWord 0xffff $val $bits
	if( $result -lt 0 )
	{
		return $result
	}

	return ( [uint16]$result )
}

function shr-word( [uint16]$val, [int]$bits )
{
	$result = shift-right $bitsPerWord 0xffff $val $bits
	if( $result -lt 0 )
	{
		return $result
	}

	return ( [uint16]$result )
}

function shl( $val, [int]$bits )
{
	if( $val -is [byte] )
	{
		return ( shl-byte $val $bits )
	}
	elseif( $val -is [int16] )
	{
		return [int16]( shl-word $val $bits )
	}
	elseif( $val -is [uint16] )
	{
		return ( shl-word $val $bits )
	}
	elseif( ( $val -lt 65536 ) -and ( $val -ge 0 ) ) ### pretend it's uint16
	{
		return ( shl-word ( [uint16]$val ) $bits )
	}

	write-error "value is an invalid type"
	return -1
}

function shr( $val, [int]$bits )
{
	if( $val -is [byte] )
	{
		return ( shr-byte $val $bits )
	}
	elseif( $val -is [int16] )
	{
		return [int16]( shr-word $val $bits )
	}
	elseif( $val -is [uint16] )
	{
		return ( shr-word $val $bits )
	}
	elseif( ( $val -lt 65536 ) -and ( $val -ge 0 ) ) ### pretend it's uint16
	{
		return ( shr-word ( [uint16]$val ) $bits )
	}

	write-error "value is an invalid type"
	return -1
}
Posted by michael | with no comments
Filed under: ,

I can't speak as to whether this is supported or not, but if you want DPM 2010 to be able to back up a SQL 2012 database instance, then you need to make a simple security change. Otherwise, DPM 2010 reports that "The replica is not consistent."

In words, NT AUTHORITY\System must be granted Sysadmin permissions on the database instance. NT AUTHORITY\System, by default, has public permissions on the instance in SQL 2012. However, in earlier versions of SQL, NT AUTHORITY\System has Sysadmin permissions at installation.

Below, the first figure identifies the specific login in the SQL Server Management Studio that has to be modified. The second figure identifies the box that needs to be checked. After checking the box, click OK to commit the change.

After this, you can open the DPM Console and for all of those SQL 2012 databases that are not consistent, go to the Protection workspace, right-click on the database(s), and then click on "Perform Consistency Check".

Figure 1 - The NT AUTHORITY\System login

Figure 2 - Setting the Sysadmin Server Instance Role

Posted by michael | with no comments
Filed under: ,

I'm working with a client that has - over an extended period of time - accumulated thousands of security groups, most of which are mail enabled for use by both Exchange and for setting rights on various objects.

Becasue they use embedded groups (that is, placing a group as a member of another group), they have a horrible time keeping straight who is actually a member of those groups.

Recently an unintentional disclosure at the client has caused them to desire to clean this mess up. :-)

They found that many tools weren't robust enough to deal with their environment. Their groups may contain thousands of members and dozens of embedded groups. And sometimes, the groups are recursive (or nested). That is, GroupA includes GroupB, GroupB includes GroupC, and GroupC includes GroupA. Also, due to historial reasons, many of their users have non-standard primary group memberships and those needed to also be included as part of the evaluation process.

Note: be aware - most tools will only return 1,500 members for a group (1,000 if your FFL is Windows 2000 mixed or Windows 2000 native). This includes most tools from Microsoft (e.g., dsquery and dsgroup). Some of the tools that handle that properly will go into an infinite loop if there are nested groups. Since the primary group is stored differently than other group memberships, most tools simply ignore it (the RID of the group is stored in the primaryGroupId attribute of a user object, instead of using a memberOf backlink or the member attribute forward link from the group itself).

We were unable to find a tool (which doesn't mean one isn't out there!) that handled all of these issues properly.

So, I wrote one. In PowerShell.

Note that performance is not great when you are scanning nested groups. This is because it is necessary to evaluate every member to determine the type of the member - user, group, contact, etc. That adds significant additional processing overhead.

Each individual piece of this script is pretty obvious (except for the "range" processing required for looking at large group memberships). But after putting it all together, it's a thing of magic. :-)

Enjoy!

###
### Get-GroupMember
###
### This function processes LARGE groups. Most normal utilities are limited
### to returning a maximum of 1,500 members for a group. To get all members
### of a group requires using a "ranged" member attribute. Few programs,
### including many from Microsoft, go to that much trouble. This one does.
###
### Also, retrieving membership from embedded groups, while avoiding the
### problems that can occur with group recursion, is something that many
### programs do not handle properly. This one does.
###
### Also, some programs do not handle empty groups properly (including the 
### example range program on MSDN from Microsoft). This one does.
###
### Also, some programs do not also check for the primaryGroupID membership, 
### and thus cannot return the membership of, for example, 'Domain Users'. 
### This one does.
###
### The ADSpath for each member of the group is written to the pipeline.
###
### Michael B. Smith
### michael at TheEssentialExchange dot come
### May, 2012
###
### Parameters:
###
###	-group 			The short name for the group. This is looked
###				up to find the distinguishedName of the group.
###
###	-ExpandEmbeddedGroups	Whether to recurse and get the membership of
###				groups contained within the parent group. If
###				this option is specified, all embedded groups
###				are scanned (including groups embedded within
###				groups embedded within groups,etc. etc.).
###
###	-Verbose		Display to the host function entry/exit and
###				status information.
###
###	-VeryVerbose		Display to the host the ADSpath of each member
###				of the group (as well as write it to the pipe).
###
###	-Statistics		Display to the host some basic statistics about
###				the query (number of users, number of embedded
###				groups, number of contacts).
###

Param(
	[string]$group	= (throw "group must be specified"),
	[switch]$ExpandEmbeddedGroups,
	[switch]$Statistics,
	[switch]$Verbose,
	[switch]$VeryVerbose
)

### for the Statistics option

$script:groupUsers    = 0
$script:groupGroups   = 0
$script:groupContacts = 0

function msg
{
	if( -not $Verbose )
	{
		return
	}

	$str = ''
	foreach( $arg in $args )
	{
		$str += $arg
	}
	write-host $str
}

function vmsg
{
	if( -not $VeryVerbose )
	{
		return
	}
	msg $args
}

function Get-PrimaryGroupID
{
	Param(
		[string]$indent,
		[string]$ADSpath
	)

	msg "${indent}Get-PrimaryGroupId: enter, ADSpath = $adspath"

	[string]$pgToken = 'primaryGroupToken'

	### format of argument: LDAP://CN=Domain Users,CN=Users,DC=smithcons,DC=local

	$groupDE  = New-Object System.DirectoryServices.DirectoryEntry( $ADSpath )
	$searcher = New-Object System.DirectoryServices.DirectorySearcher( $groupDE )
	$searcher.Filter = "(objectClass=*)"

	$searcher.PropertiesToLoad.Add( $pgToken ) | Out-Null

	$result = $searcher.FindOne()
	if( $result -ne $null )
	{
		if( $result.Properties.Contains( $pgToken ) -eq $true )
		{
			msg "${indent}Get-PrimaryGroupId: exit, token = $($result.Properties.primarygrouptoken)"

			return $result.Properties.primarygrouptoken
		}
	}

	msg "${indent}Get-PrimaryGroupId: exit, token not found"
	return 0
}

function Search-PrimaryGroupID
{
	Param(
		[string]$indent,
		[string]$namingContext,
		[int]$primaryGroup,
		[hashtable]$dictionary
	)

	msg "${indent}Search-PrimaryGroupId: enter, namingcontext = '$namingContext', primaryGroup = $primaryGroup"

	$ldapFilter = "(primaryGroupID=$primaryGroup)"

	$directorySearcher = New-Object System.DirectoryServices.DirectorySearcher
	$directorySearcher.PageSize    = 1000
	$directorySearcher.SearchRoot  = ( "LDAP://" + $namingContext )
	$directorySearcher.SearchScope = "subtree"
	$directorySearcher.Filter      = $ldapFilter

	### load the properties we want

	$directorySearcher.PropertiesToLoad.Add( "distinguishedName" ) | Out-Null
	$directorySearcher.PropertiesToLoad.Add( "objectClass" )       | Out-Null

	$results = $directorySearcher.FindAll()
	if( $results -ne $null )
	{
		msg "${indent}Search-PrimaryGroupId: found $($results.Count) results"
		foreach( $result in $results )
		{
			$myadspath   = $result.Path
			$objCount    = $result.Properties.objectclass.count
			$objectClass = $result.Properties.objectclass[ $objCount - 1 ]

			if( $objectClass -eq 'user' )
			{
				if( $dictionary.$myadspath -eq 1 )
				{
					msg "${indent}Search-PrimaryGroupID: continue duplicate user"
					return
				}
				$dictionary.$myadspath = 1
				$script:groupUsers++
				write-output $myadspath
				vmsg "${indent}Search-PrimaryGroupId: $myadspath"
			}
			else
			{
				write-error "Invalid objectclass for primarygroupid: $objectClass"
			}
		}
	}
	else
	{
		msg "${indent}Search-PrimaryGroupID: result from FindAll() was null"
	}

	msg "${indent}Search-PrimaryGroupId: exit"
}

function Search-Group
{
	Param(
		[string]$indent,
		[string]$ADSpath,
		[hashtable]$dictionary
	)

	### based originally on http://msdn.microsoft.com/en-us/library/bb885125.aspx
	### but has bug-fixes and enhancements

	msg "${indent}Search-Group: enter, $ADSpath"

	$groupDE  = New-Object System.DirectoryServices.DirectoryEntry( $ADSpath )
	$searcher = New-Object System.DirectoryServices.DirectorySearcher( $groupDE )
	$searcher.Filter = "(objectClass=*)"

	[bool]$lastLoop = $false
	[bool]$quitLoop = $false

	[int]$step = 999
	[int]$low  = 0
	[int]$high = $step

	do {
		if( $lastLoop -eq $false )
		{
			[string]$member = 'member;range=' + $low.ToString() + '-' + $high.ToString()
		}
		else
		{
			[string]$member = 'member;range=' + $low.ToString() + '-' + '*'
		}
		msg "${indent}Search-Group: member = $member"

		$searcher.PropertiesToLoad.Clear()        | Out-Null
		$searcher.PropertiesToLoad.Add( $member ) | Out-Null

		$result = $searcher.FindOne()
		if( $result -eq $null )
		{
			### not sure what to do here
			msg "${indent}Search-Group: searcher failure"
			break
		}

		if( $result.Properties.Contains( $member ) -eq $true )
		{
			$entries = $result.Properties.$member
			msg "${indent}Search-Group: entries.Count = $($entries.Count)"
			foreach( $entry in $entries )
			{
				if( $ExpandEmbeddedGroups )
				{
					$memberObj   = [ADSI] "LDAP://$entry"
					$objectClass = $memberObj.objectClass.Item( $memberObj.objectClass.Count - 1 )
					$myadspath   = $memberObj.Path
					$memberObj   = $null
				}
				else
				{
					$myadspath   = $entry
					$objectClass = 'user'
				}
				write-output $myadspath ### output to pipeline

				switch( $objectClass )
				{
					'group'
						{
							if( $dictionary.$myadspath -eq 1 )
							{
								msg "${indent}Search-Group: continue duplicate group"
								continue
							}
							$dictionary.$myadspath = 1
							$script:groupGroups++
							vmsg "${indent}Search-Group: group $myadspath"
							Search-Group ( $indent + '  ' ) $myadspath $dictionary
						}
					'contact'
						{
							if( $dictionary.$myadspath -eq 1 )
							{
								msg "${indent}Search-Group: continue duplicate contact"
								continue
							}
							$dictionary.$myadspath = 1
							$script:groupContacts++
							vmsg "${indent}Search-Group: contact $myadspath"
						}
					'user'
						{
							if( $dictionary.$myadspath -eq 1 )
							{
								msg "${indent}Search-Group: continue duplicate user"
								continue
							}
							$dictionary.$myadspath = 1
							$script:groupUsers++
							vmsg "${indent}Search-Group: user $myadspath"
						}
					'foreignSecurityPrincipal'
						{
							### do nothing
						}
					default
						{
							write-error "Search-Group: unhandled objectClass as member of group: $objectClass"
						}
				}
			}

			### could just say: $quitLoop = $lastLoop
			### but it's not a worthwhile optimization
			### (due to a loss of clarity in WHY)

			if( $lastLoop -eq $true )
			{
				msg "${indent}Search-Group: set quitLoop = true"
				$quitLoop = $true
			}
		}
		else
		{
			if( $lastLoop -eq $true )
			{
				msg "${indent}Search-Group: set quitLoop = true"
				$quitLoop = $true
			}
			else
			{
				msg "${indent}Search-Group: set lastLoop = true"
				$lastLoop = $true
			}
		}

		if( $lastLoop -eq $false )
		{
			msg "${indent}Search-Group: old low = $low, old high = $high"
			$low  = $high + 1
			$high = $low  + $step
			msg "${indent}Search-Group: new low = $low, new high = $high"
		}

	} until( $quitLoop -eq $true )

	$object   = $null
	$searcher = $null
	$groupDE  = $null

	$primaryID = Get-PrimaryGroupId $indent $ADSpath
	if( $primaryID -gt 0 )
	{
		Search-PrimaryGroupId $indent $script:defaultNC $primaryId $dictionary
	}

	msg "${indent}Search-Group: exit, $ADSpath"
}

function Search-ADForGroup
{
	Param(
		[string]$indent,
		[string]$group
	)

	msg "${indent}Search-ADForGroup: enter, group = $group"

	### build the LDAP search to find the group distinguishedName from the provided short name

	$rootDSE    = [ADSI]"LDAP://RootDSE"
	$defaultNC  = $rootDSE.defaultNamingContext
	$ldapFilter = "(&(objectCategory=group)(name=$group))"
	$rootDSE    = $null

	$directorySearcher = New-Object System.DirectoryServices.DirectorySearcher
	$directorySearcher.PageSize    = 1000
	$directorySearcher.SearchRoot  = ( "LDAP://" + $defaultNC )
	$directorySearcher.SearchScope = "subtree"
	$directorySearcher.Filter      = $ldapFilter

	### Define the property we want (if we don't specify at least one property,
	### then "all default properties" get loaded - that's slower).

	$directorySearcher.PropertiesToLoad.Add( "distinguishedName"  ) | Out-Null

	$groups = $directorySearcher.FindAll()
	if( $groups -eq $null )
	{
		write-error "Search-ADForGroup: No such group found: $group"
	}
	elseif( $groups.Count -eq 1 )
	{
		$script:defaultNC = $defaultNC
		msg "${indent}Search-ADForGroup: exit, $($groups.Item( 0 ).Path)"

		return $groups.Item( 0 ).Path ### same as ADSpath in VBScript
	}
	else
	{
		write-error "Search-ADForGroup: Multiple groups were found that match: $group"
	}

	msg "${indent}Search-ADForGroup: exit, null"
	return $null
}

	###
	### Main
	###

	if( $VeryVerbose )
	{
		$Verbose = $true
	}

	$result = Search-ADForGroup '' $group
	if( $result -ne $null )
	{
		$dictionary = @{}

		Search-Group '' $result $dictionary

		if( $Statistics )
		{
			write-host " "
			write-host "Users: $($script:groupUsers)"
			write-host "Groups: $($script:groupGroups)"
			write-host "Contacts: $($script:groupContacts)"
		}

		$dictionary = $null
	}

I'm in the process of developing a large new script and to test it properly, I needed to create several thousand new users with mailboxes. While I've previously written scripts like this for Exchange 2003, I didn't have one handy for Exchange 2007 and later.

So I whipped one up and decided to share! As you can probably tell, this script will create, by default, 6,000 users and mailboxes. You do have to tell it the password to assign, and you should change the $upnDomain to be a proper choice for your environment.

Enjoy!

Param(
	[int]$minValue =    1,
	[int]$maxValue = 6000,
	[string]$upnDomain = 'smithcons.local'
)

$password = ( get-credential UserName-Not-Important ).password

for( $i = $minValue; $i -le $maxValue; $i++ )
{
	$user = "User" + $i.ToString()
	New-Mailbox $user `
		-alias $user `
		-userprincipalname ( $user + '@' + $upnDomain ) `
		-samaccountname $user `
		-firstname $user `
		-lastname $user `
		-password $password
}

'Done'
Posted by michael | with no comments