Update the default GPO permissions

This article ( https://support.microsoft.com/en-us/kb/321476) explains how to alter the default permissions on all new GPOs you create however it doesn’t really explain what you are doing which means at best you didn’t learn anything while doing it and at worse didn’t actually achieve what you set out to do.  the article also doesn’t tell you how to deal with all the existing GPOs either and doing this manually is just not going to scale. Doubly worse is that the SDDL they publish is actually incorrect and has errors in it.

The internet is full of mis-information ( hopefully this article is not compounding that )

They tell you to use this SDDL:
D:P(A;CI;RPLCLOLORC;;;DA)(A;CI;RPWPCCDCLCLOLORCWOWDSDDTSW;;;EA)(A;CI;RPWPCCDCLCLOLORCWOWDSDDTSW;;;CO)(A;CI;RPWPCCDCLCLORCWOWDSDDTSW;;;SY)(A;CI;RPLCLORC;;;AU)(OA;CI;CR;edacfd8f-ffb3-11d1-b41d-00a0c968f939;;AU)

I’ve highlighted the repeated permissions – clearly that’s not right.

In messing with my system, and people had already been updating the schema before I started meddling too, I realised I could not find the ‘Out of the box’ permissions published anywhere on the tinternet!  So here it is should you need it.  I got this from a default out of the box domain I had lying around :-).

D:P(A;CI;CCDCLCSWRPWPDTLOSDRCWDWO;;;DA)(A;CI;CCDCLCSWRPWPDTLOSDRCWDWO;;;EA)(A;CI;CCDCLCSWRPWPDTLOSDRCWDWO;;;CO)(A;CI;CCDCLCSWRPWPDTLOSDRCWDWO;;;SY)(A;CI;LCRPLORC;;;AU)(OA;CI;CR;edacfd8f-ffb3-11d1-b41d-00a0c968f939;;AU)(A;CI;LCRPLORC;;;ED)

Why might I want to change the default GPO permissions?

The main reason is to support the least privilege paradigm.  By default only Domain Admins can create and administer GPOs.  Microsoft thoughtfully set up a special group that allows you to delegate the right to create GPOs called the ‘Group Policy Creator Owners’ Group.  However you also need the right to link group policy to an OU but even then only the person who creates the GPO and the domain admins can administer the GPO.  That’s not particularly good is it?  ( Clint Boessen’s  blog described the problem for you )

Education or dedication, administrator will always do what they think is right so we need to help them get it right first time

These problems can be solved by educating the users that create the GPOs to delegate access to the GPOs to groups.  The issue with that is we are all human.  In a large environment there is little you can do to solve this.  In small environments you can definitely improve on this.  Even in large environments this will improve things by removing the need to be a domain admin to administer all the group policies in your environment.

Set up the AAD groups to delegate EDIT and MANAGE GPO rights

Start by creating three groups in AD, gpo-editGPLink, gpo-editors and gpo-managers.  Add the gpo-editors and gpo-managers groups to the builtin Group Policy Creator Owners group.  This will allow them to create GPOs but not link them.   Delegate the write gpLink to the domain object and add the gpo-Managers group to the gpo-editGPLink  group.

Determine the SDDL you want to add to the default setting

That’s the groups set up now modify the schema ( carefully of course ) so that the two groups are added to all new GPOs.

Consider using the ‘clean’ default above if your schema has already been modified

When I ‘googled’ for the way to do this I only found people delegating how to delegate MODIFY and no one seemed to show the SDDL for an EDIT ACE or how to change the default owner which is Domain Admins by the way.

The method I used was this.

  1. Create a NEW GPO
  2. Use PowerShell to determine the current SDDL
    1. View the GPO in GPMC and select the details tab
    2. Get the UniqueID ( which will be a GUID )
    3. Use the following command
      $(Get-ACL “AD:/<GPOGUID>,CN=Policies,CN=System,DC=AD,DC=COM”).sddl
    4. Save the SDDL in case you need to roll back the changes
    5. Modify the ACL on the NEW GPO adding the EDIT and MODIFY group with appropriate delegation
    6. Set the GPO owner as the MODIFY group
    7. Use the same command as above to grab the new SDDL
    8. it will now have 2 new ACEs with SIDs in, one for the EDIT and MANAGER groups

Check that the owner definition is also updated to one of the SIDs

The SDDL header becomes O:<GroupSID>G:DAD:PAI

Your SDDL should look like this:
O:G:DAD:PAI(A;CI;CCDCLCSWRPWPDTLOSDRCWDWO;;;DA)(A;CI;CCDCLCSWRPWPDTLOSDRCWDWO;;;EA)(A;CI;CCDCLCSWRPWPDTLOSDRCWDWO;;;CO)(A;CI;CCDCLCSWRPWPDTLOSDRCWDWO;;;SY)(A;CI;LCRPLORC;;;AU)(OA;CI;CR;edacfd8f-ffb3-11d1-b41d-00a0c968f939;;AU)(A;CI;LCRPLORC;;;ED)(A;CI;RPWPCCDCLCLOLORCWOWDSDDTSW;;;)(A;CI;CCDCLCRPWPRC;;;)

The Creator Owner SDDL may actually be (A;CIIO;CCDCLCSWRPWPDTLOSDRCWDWO;;;CO) but that’s because it’s applied to a single GPO it should be:
(A;CI;CCDCLCSWRPWPDTLOSDRCWDWO;;;CO) in the default.

NOTE all we really did was add 2 new ACES to the current default
(A;CI;RPWPCCDCLCLOLORCWOWDSDDTSW;;;)
(A;CI;CCDCLCRPWPRC;;;)
and modified the default Owner O:G:DAD:PAI in the SDDL header.

Now update the schema

CN=Group-Policy-Container,CN=Schema,CN=Configuration,DC=ad,DC=man,DC=com

Using ADSIEdit.msc and paste the required SDDL into the attribute value.

NOTE: You need to be a schema admin to do this obviously.

The last bit is to reset the permissions on each GPO that already exists and the easy way to do this is to use PowerShell.

Dell publish a script which can be found here.

https://support.software.dell.com/gpoadmin/kb/209382 however this too only adds a single security principle which Dell suggest is the GPO Admin service account.  You can easily modify the script to add both permissions as follows:

 

$domain = “<YourDOmain>”

$Manager = “gpo-Managers”

$editor = “gpo-Editors”

 

function Usage {

Grants the specified service account Edit settings, delete, and modify security and assigns ownership to all the GPOs in the specified domain.

 

GPOADmin.AddServiceAccountToAllGPOs -Domain <string> -ServiceAccount <string>

-Domain: Specifies the DNS name of the domain in which to modify the GPOs.

-ServiceAccount: Specifies the account in domain\user format that will be

granted access to and made the owner of all the GPOs.

 

Example:

GPOADmin.AddServiceAccountToAllGPOs -Domain “MyDomain.com” -ServiceAccount “mydomain\Service Account”

 

exit 1

}

 

 

if( !$Domain -or !$Manager)

{

Usage

}

 

Function UpdateGPOPermissions

{

param( [System.Object] $gpo)

$Script:result = $true

Trap [System.Exception]

{

write-host $_.Exception.Message;

$Script:result = $false

}

# Assign the new permission.

$gpmGPOSecurityInfo = $gpo.GetSecurityInfo()

$gpmGPOSecurityInfo.Add($gpmGPOEditSecurityAndDeletePermission)

$gpmGPOSecurityInfo.Add($gpmGPOEditSecurity)

$gpo.SetSecurityInfo($gpmGPOSecurityInfo)

 

# Make the service account the owner.

$adGPO = new-Object -typename System.DirectoryServices.DirectoryEntry(“LDAP://” + $Domain + “/” +$gpo.Path);

$owner = $adGPO.ObjectSecurity.GetOwner([System.Security.Principal.NTAccount]);

$adGPO.ObjectSecurity.SetOwner( $managerAccount );

$adGPO.CommitChanges();

return $Script:result

}

 

# Create an NTAccount object using the specified service account.

$managerAccount= new-object system.security.principal.ntaccount($Manager)

$editorAccount= new-object system.security.principal.ntaccount($editor)

 

# Create the GPMC Main object.

$gpm = New-Object -ComObject GPMgmt.GPM

 

# Load the GPMC constants.

$gpmConstants = $gpm.GetConstants()

 

# Connect to the domain passed using any DC.

$gpmDomain = $gpm.GetDomain($Domain, “”, $gpmConstants.UseAnyDC)

 

# Create a new empty instance of a search criteria.

$gpmSearchCriteria = $gpm.CreateSearchCriteria()

 

# Perform a search with an empty search criteria to return ALL GPOs.

$gpmGPOs = $gpmDomain.SearchGPOs($gpmSearchCriteria)

 

# Create a new permission granting the service account Edit, Modify Security, and Delete.

$gpmGPOEditSecurityAndDeletePermission = $gpm.CreatePermission($Manager, $gpmConstants.permGPOEditSecurityAndDelete, $true)

$gpmGPOEditSecurity = $gpm.CreatePermission($editor, $gpmConstants.permGPOEdit , $true)

 

# For each GPO…

foreach( $gpmGPO in $gpmGPOs)

{

trap

{

Write-Host $_.Exception.Message;

if( $_.Exception.InnerException -ne $null)

{

Write-Host $_.Exception.InnerException.Message;

}

continue;

}

# Update the status.

Write-Host -NoNewline (“Granting the service account ‘$Manager’ access to the GPO ‘{0}’…” -f $gpmGPO.DisplayName)

# Grant permissions for the current GPO.

$result = UpdateGPOPermissions $gpmGPO

if( $result -eq $true)

{

Write-Host Success!

}

else

{

Write-Host Failed!

}

}

 

 

 

 

 

 

 

 

Advertisements

SDDL Explained

Security Descriptor Definition Language

SDDL is a way of defining permissions that can be applied to objects.  This includes information about the object’s owner and who can access the object and in what way.

A security descriptor contains a Discretionary Access Control List (DACL) and a System Access Control List (SACL).

An ACL is a list of ordered Access Control Entries (ACE) that specify DACL and SACLs.

A DACL identifies users and groups who are allowed or denied access to an object and in what way the object is accessed.

The SACL defines how access is audited on an object.

Example: SDDL O:DAG:DAD:PAI (A;OICIIO;FA;;;CO) (A;OICI;0x1200a9;;;ED) (A;OICI;0x1200a9;;;AU) (A;OICI;FA;;;SY) (A;OICI;FA;;;DA) (A;OICI;FA;;;EA)

An SDDL string is composed of 5 parts:

The Header – The header contains flags that designate whether the object is allowing or blocking inheritance for the SACL and DACL.

DACL (D:) – The Discretionary Access Control List denoted by the (D:)

SACL (S:) – The System Access Control List denoted by the (S:)

Primary Group (G:) – This value is still in the security descriptor for compatibility reasons. Windows 2000/2003 does not rely on this part of the security descriptor unless you are using services for UNIX and/or Macintosh with tools and utilities applying thereto.

Owner (O:) – Indicates which trustee owns the object. A trustee is the user account, group account, or logon session to which an access control entry (ACE) applies. Each ACE in an access control list (ACL) has one security identifier (SID, also called a principal) that identifies a trustee. The value is represented in SID string format. A security identifier (SID) identifies a user, a group, or a logon session. Each user has a unique SID, which is retrieved by the operating system at logon.

Header

The DACL inheritance can be set using PAI where P sets an SDDL_PROTECTED flag, that means that Inheritance is blocked. AI sets SDDL_AUTO_INHERITED means that Inheritance is allowed as long as P isn’t set.

ACE

ACE’s are enclosed within parenthesis. There are 6 fields in each ACE. These 6 fields are separated by a semicolon delimiter.

The fields are as follows:

  • ACE type (allow/deny/audit)
  • ACE flags (inheritance and audit settings)
  • Permissions (list of incremental permissions)
  • ObjectType (GUID)
  • Inherited Object Type (GUID)
  • Trustee (SID)

the parts are concatenated using the semi colon e.g.

(A;OICIIO;FA;;;CO)

A; = Allow

OICIIO; = Inheritance flags ( Object Inherit, Container Inherit, Inherit Only )

FA = Full Access

;; = Often the Object Type and Inherited Type are not set and are missing from the SDDL ACE as in this example.

CO = shorthand for Creator Owner.  Many Well Known SIDs have shorthand notations

ACE Flags

The ACE flags denote the inheritance options for the ACE, and if it is a SACL, the audit settings.

 

Value
Description
“CI” CONTAINER INHERIT: Child objects that are containers, such as directories, inherit the ACE as an explicit ACE.
“OI” OBJECT INHERIT: Child objects that are not containers inherit the ACE as an explicit ACE.
“NP” NO PROPAGATE: ONLY IMMEDIATE CHILDREN INHERIT THIS ACE.
“IO” INHERITANCE ONLY: ACE DOESN’T APPLY TO THIS OBJECT, BUT MAY AFFECT CHILDREN VIA INHERITANCE.
“ID” ACE IS INHERITED
“SA” SUCCESSFUL ACCESS AUDIT
“FA” FAILED ACCESS AUDIT

 ACE Types

The ACE type designates whether the trustee is allowed, denied or audited.

Value
Description
“A” ACCESS ALLOWED
“D” ACCESS DENIED
“OA” OBJECT ACCESS ALLOWED: ONLY APPLIES TO A SUBSET OF THE OBJECT(S).
“OD” OBJECT ACCESS DENIED: ONLY APPLIES TO A SUBSET OF THE OBJECT(S).
“AU” SYSTEM AUDIT
“AL” SYSTEM ALARM
“OU” OBJECT SYSTEM AUDIT
“OL” OBJECT SYSTEM ALARM

Permissions

The Permissions are a list of the incremental permissions given (or denied/audited) to the trustee-these correspond to the permissions discussed earlier and are simply appended together. However, the incremental permissions are not the only permissions available. The table below lists all the permissions.

Value Description Hexadecimal Value Binary Bits from 0
Generic access rights
“GA” GENERIC ALL 0x10000000 Bit 28
“GR” GENERIC READ 0x80000000 Bit 31
“GW” GENERIC WRITE 0x40000000 Bit 30
“GX” GENERIC EXECUTE 0x20000000 Bit 29
Directory service access rights
“RC” Read Permissions 0x20000 Bit 17
“SD” Delete 0x10000 Bit 16
“WD” Modify Permissions 0x40000 Bit 18
“WO” Modify Owner 0x80000 Bit 19
“RP” Read All Properties 0x00000010 Bit 4
“WP” Write All Properties 0x00000020 Bit 5
“CC” Create All Child Objects 0x00000001 Bit 0
“DC” Delete All Child Objects 0x00000002 Bit 1
“LC” List Contents 0x00000004 Bit 2
“SW” All Validated Writes 0x00000008 Bit 3
“LO” List Object 0x00000080 Bit 7
“DT” Delete Subtree 0x00000040 Bit 6
“CR” All Extended Rights 0x00000100 Bit 8
File access rights
“FA” FILE ALL ACCESS
“FR” FILE GENERIC READ
“FW” FILE GENERIC WRITE
“FX” FILE GENERIC EXECUTE
Registry key access rights
“KA” KEY ALL ACCESS 0xF003F
“KR” KEY READ 0x20019
“KW” KEY WRITE 0x20006
“KX” KEY EXECUTE 0x20019
KEY CREATE SUB KEYS 0x0004
KEY ENUMERATE SUB KEYS 0x0008
KEY NOTIFY 0x0010
KEY QUERY VALUE 0x0001
KEY SET VALUE 0x0002

 

What does it mean when the ROOT certificate authority tell you that a SHA256 root authority might be less compatible than a SHA1 root?

We just ordered a new cert from an external trusted authority and were presented with a choice. We could get a SHA256 cert issued from a chain where the root CA certificate was SHA1 or one from a SHA256 root CA. The text under the SHA1 root option said that we should choose this for greater compatibility.

Well we went for the SHA256 root because who wants a legacy root CA a?

Just to let you know what the incompatibility might be, because at the time we could not think of why we might have issues.

The problem is that the SHA256 root CA is relatively new. Not everyone will trust the Root CA as the trusted root stores are not always automatically updated.

Not really compatibility then just some people are not actively updating the trusted root CA stores.

FYI there are a number of ways of doing this here’s a links on it you might find useful.

http://netsekure.org/2011/04/automatic-ca-root-certificate-updates-on-windows/

We went for just delivering the one cert by GPO rather than updating the trusted roots as we needed to do it quickly to support this new cert and we plan to test the best route for keeping them up to date going forward. Our workstations are fine as they have an internet connection but most servers are blocked for obvious reasons.

Enumeration of AD or ARS objects or Don’t tell the commandlet what you are looking for!

A recent comment to one of my posts was also a question.   Interestingly I had a colleague at work ask me almost the same question the other day.  Clearly there’s a gap in the market, ripe for a blog post 🙂 .

This is something you will kick you self about because it’s obvious really once you know how.

How can I enumerate all the objects in a container?

The question was how can I enumerate all the objects in a container.  The answer is don’t tell the commandlet what you are looking for.  OK that’s obvious isn’t it?  Well it is, if you step back and look at why you’re not getting the child objects returned.

If I use Get-QADObject <DN of an OU or Container> then I’m going to get just that OU or container object returned.  The object will have a property ParentContainer and another ParentContainerDN but you won’t find any property or method that will list the child objects, if any exist.

So how do I list all the child objects? Use the searchRoot switch.

Don’t give the commandlet an absolute object reference use the searchRoot switch instead and specify the top level container.

Remember your interviewing skills…. don’t ask closed questions or you’ll get yes or no back as an answer which won’t always be useful.

 

use the command line switches to filter the result set

Use:
-type to filter the types of object returned
-searchScope to list oneLevel ( just objects in the immediate container)  or subTree ( all child objects – this is the same as Dir /s )

In the case of an ARS scheduled task it looks like the scheduledTask and scheduledTasksContainer are both the same object type, thanks Dell.

Use Get-Member to list properties of the returned objects.

If you come across this then use Get-Member to see if there are other properties that might help to filter the results.  The classname and a WHERE clause will solve that for ARS scheduled tasks and containers.

Actually I take back that last comment about the task and the container being the same type.  I used .getType() and this returned what looked like the same object type, and ARSDirectoryObject but actually  when I used Get-Member I could see the type was listed as  edsScheduledTasksContainer and edsScheduledTask  and I checked, you can use these as a values for the type switch and it works fine.

I said it was obvious didn’t I?

 

Restoring the Windows Security button to a 2012 server

Previously I’d made notes on how to do this on a single server
Adding the Windows Security button to 2012

The problem is that this was per server so not really scalable.   No problem you should be able to use GPO.   Create a file share and just deliver the shortcut to all your servers.  But what about the appsFolder.itemdata-ms file.  Hmm try as I might I couldn’t get this to work.  So I came up with a work around.

I deliver the shortcut to a folder on all the servers and then create a shortcut and point the shortcut at the short cut.  Seems to work for me.

If you’d like me to post the full details of how I did this just let me know.  I’ll be posting about how to stop people accidently shutting down your server as happens way too often ( once is too often in any production environment ) and this also includes using this method for delivering a shortcut to a script.

 

Blogging 101 Day One: Introduce yourself to the world

Having enrolled in the blogging 101 to see if I can improve my blog here’s the first assignment.

  • Why are you blogging publicly, rather than keeping a personal journal?

So this one is easy, it’s to shorten my google search to just my blog for anything ARS / Powershell or AD related.  Like that’s going to happen any time soon.  This is where I’m posting all the things I find out in the hope that other people can make use of it and save them the pain of solving it themselves

  • What topics do you think you’ll write about?

Another easy one, come on surely you can think of a harder question?  ARS, PowerShell and AD related topics oh and my new app Password Manager Pro.  There’s lots of things wrong with PMP so I can see a lot of posts on this in the coming months.

  • Who would you love to connect with via your blog?

Other IT people working in the same area as me.  I’d actually like, in some instances to get feed back on possible solutions that I post.

  • If you blog successfully throughout the next year, what would you hope to have accomplished?

Actually I’m not that organised in that I have a goal that I want to achieve with the blog.  Which is good, right as I can’t fail either.  Who said if you fail to plan you plan to fail.  They got that wrong didn’t they 🙂

Anyways, Happy New Year!

Lee

 

 

 

 

Preventing non admin accounts being added to a group using Active Roles

Here is a function I use to prevent non admin users being added to a group

You can modify this to get the user object and then take appropriate action –

In my case the DN was CN=admin- so I realised I didn;t need to actuially bind to the user object but I left in my original code should I need it later – you can see my original get-qaduser call is commented out

# $member will be the DN
# $user = Get-QADUser $member

NOTE: if you are allowing changes externally to the group as is the case in my environment you also need an onPostModify ($Request)  function to deal with changes syncing in from other DCs.

function onPreModify($Request)
{
# this function handles updates carried out in ARS – prevents non admin user being added
Out-DebugString -verbosity 9 -str “group-Enforce-Admin-Only PreModify >>> Check if this was a group modification”
if ($Request.class -eq “group”)
{
Out-DebugString -verbosity 9 -str “group-Enforce-Admin-Only PreModify >>> Group object modified”
if ( $(Get-IsAttributeModified -AttributeName ‘member’ -Request $Request) )
{
Out-DebugString -verbosity 9 -str “group-Enforce-Admin-Only PreModify >>> Checking if member attribute was modified”
for ($i = 0; $i -lt $Request.PropertyCount; $i++) {
$item = $Request.Item($i)
$Name = $item.Name
Out-DebugString -verbosity 9 -str “group-Enforce-Admin-Only PreModify >>> item = $($item) name = $($name)”
# enumerate the objects attributes until we locate the “member” attribute
if ($Name -eq “member”)
{
# check that the member attribute was modified
Out-DebugString -verbosity 9 -str “group-Enforce-Admin-Only PreModify >>> Found Member Attribute so processing ”
Out-DebugString -verbosity 9 -str “group-Enforce-Admin-Only PreModify >>> item control code = $($item.ControlCode) ”
Out-DebugString -verbosity 9 -str “group-Enforce-Admin-Only PreModify >>> Constant APEND =  $($Constants.ADS_PROPERTY_APPEND) ”
if($item.ControlCode -eq $Constants.ADS_PROPERTY_APPEND )
{
# iterate through the group members
Out-DebugString -verbosity 9 -str “group-Enforce-Admin-Only PreModify >>> item control code was APPEND so processing…… ”
foreach ($member in $item.Values) {
Out-DebugString -verbosity 9 -str “group-Enforce-Admin-Only PreModify >>> member = $($member)”
Out-DebugString -verbosity 9 -str “group-Enforce-Admin-Only PreModify >>> left(member,9) = $($member.substring(0,9))”
Out-DebugString -verbosity 9 -str “group-Enforce-Admin-Only PreModify >>> left(member,10) = $($member.substring(0,10))”
# $member will be the DN
#$user = Get-QADUser $member
#if ( $($user.sAMAccountName).substring(0,6) -ne “admin-” )
if ( ( $member.substring(0,9) -ne “CN=Admin-” ) -and  ( $member.substring(0,10) -ne “CN=Admin -” ) -and ( $member.substring(0,7) -ne “CN=svc-” ) ) {
$groupName = Get-AttributeValue -AttributeName “Name” -ADSIObject $Request
$groupDN = Get-AttributeValue -AttributeName “DN” -ADSIObject $Request
Out-DebugString -verbosity 9 -str “group-Enforce-Admin-Only PreModify >>> >> Grp Name : $($groupname) DN: $($groupDN)”
throw “******************************************************`nYou cannot add non ‘admin-<logon name>’ accounts `nto this group`n******************************************************`nNOTE: Script Policy Checks the DN of the user not the logon name”
}
} # end for each
} # end if property changed
} # end if member found
} # end for iterate group members
} # end Check if group members updated
} # end if modified object was a group
} # end function

 

function onPostModify ($Request)
{
# this fucntion deals with changes made outside of ARS…
if ($Request.class -eq “group”)
{
Out-DebugString -verbosity 9 -str “group-Enforce-Admin-Only postModify >>> Group object modified”
if ( $(Get-IsAttributeModified -AttributeName ‘member’ -Request $Request) )
{
Out-DebugString -verbosity 9 -str “group-Enforce-Admin-Only postModify >>> Group membership changed”
for ($i = 0; $i -lt $Request.PropertyCount; $i++) {
$item = $Request.Item($i)
$Name = $item.Name
Out-DebugString -verbosity 9 -str “group-Enforce-Admin-Only postModify >>> item = $($item) name = $($name)”
# enumerate the objects attributes until we locate the “member” attribute
if ($Name -eq “member”)
{
# check that the member attribute was modified
Out-DebugString -verbosity 9 -str “group-Enforce-Admin-Only postModify >>> Found Member Attribute so processing ”
Out-DebugString -verbosity 9 -str “group-Enforce-Admin-Only postModify >>> item control code = $($item.ControlCode) ”
Out-DebugString -verbosity 9 -str “group-Enforce-Admin-Only postModify >>> Constant ADS_PROPERTY_APPEND =  $($Constants.ADS_PROPERTY_APPEND) ”
if( $item.ControlCode -eq $Constants.ADS_PROPERTY_APPEND )
{
# iterate through the group members
Out-DebugString -verbosity 9 -str “group-Enforce-Admin-Only postModify >>> item control code was APPEND so processing…… ”
foreach ($member in $item.Values) {
Out-DebugString -verbosity 9 -str “group-Enforce-Admin-Only postModify >>> member = $($member)”
Out-DebugString -verbosity 9 -str “group-Enforce-Admin-Only postModify >>> left(member,9) = $($member.substring(0,9))”
Out-DebugString -verbosity 9 -str “group-Enforce-Admin-Only PostModify >>> left(member,10) = $($member.substring(0,10))”
# $member will be the DN
if ( ( $member.substring(0,9) -ne “CN=Admin-” ) -and  ( $member.substring(0,10) -ne “CN=Admin -” ) -and ( $member.substring(0,7) -ne “CN=svc-” ) ) {
$groupName = Get-AttributeValue -AttributeName “Name” -ADSIObject $Request
Out-DebugString -verbosity 9 -str “group-Enforce-Admin-Only postModify >>> GroupName = $($groupname)”
Remove-QADGroupMember -Member $member -Proxy -Identity $groupName -Control @{OperationReason=”Group – Enforce Admin Users Only_v1.1″}
} # end check if the user is not an admin- user
} # end processing each member added
} # end if members were added – ADS_PROPERTY_APPEND = 3
} # end if member found
} # end for iterate group members updated
} # end check that the group membership was updated
} # end check that request object was a group object
}

I’ve included my debug function for you here too – this writes to the event log – these go at the top of the script file

function onInit($Context)
{
$par01 = $context.AddParameter(“debugging”)
$par01.MultiValued = $false
$par01.PossibleValues = “0”, “1”, “2”, “3”, “4”, “5”, “6”, “7”, “8”, “9”
$par01.DefaultValue = “1”
$par01.Description = “Debugging EventLog Level where: 0 is no debugging; 9 is the most verbose; 1 is the least verbose”
$par01.Required = $false
#
#
}

function Out-DebugString([int]$verbosity, [string]$str )
{
# outputs debug info to the EDM event log
if ( [string]$PolicyEntry.Parameter(“debugging”) -ne ‘0’ )
{
$strDebuggingSwitch = [string]$PolicyEntry.Parameter(“debugging”)
if ( $verbosity -le [int]$strDebuggingSwitch )
{
$EventLog.ReportEvent(2,$str)
}
}

function Get-IsAttributeModified ([string]$AttributeName, $Request)
{
$objEntry = $Request.GetPropertyItem($AttributeName, $Constants.ADSTYPE_CASE_IGNORE_STRING)
if ($objEntry -eq $null) { return $false }
if ($objEntry.ControlCode -eq 0) { return $false }
return $true
} #– Get-IsAttributeModified