public/Add-OUStructureFromTemplate.ps1

Function Add-OUStructureFromTemplate {
    [CmdletBinding(DefaultParameterSetName = "NonDefaultTemplate", SupportsShouldProcess = $true)]
    Param
    (
        # Name of the OU structure. This is the CN of the parent OU and is used to derive the middle part of principal names (for global / orgs / components)
        [Parameter( ParameterSetName = "NonDefaultTemplate", Mandatory, ValueFromPipelineByPropertyName )]
        [Parameter( ParameterSetName = "AsOrg", Mandatory, ValueFromPipelineByPropertyName )]
        [Parameter( ParameterSetName = "AsComponent", Mandatory, ValueFromPipelineByPropertyName )]
        [String]$Name,

        # Description that will be added to the parent OU's LDAP description attribute
        [Parameter( ValueFromPipelineByPropertyName)]
        [String]$Description,

        # The parent path, for generic OU structures
        [Parameter( ParameterSetName = "NonDefaultTemplate", Mandatory, ValueFromPipelineByPropertyName )]
        [String]$Path,

        # The hashtable template. TODO: make this a custom class.
        [Parameter( ParameterSetName = "NonDefaultTemplate", Mandatory, ValueFromPipelineByPropertyName )]
        [System.Collections.Hashtable]$Template,

        # Use the 'Global' template and set the parent to none
        [Parameter( ParameterSetName = "AsGlobal", Mandatory, ValueFromPipelineByPropertyName )]
        [switch]
        $AsGlobal,

        # Use the 'org' template and set the parent to global
        [Parameter( ParameterSetName = "AsOrg", Mandatory, ValueFromPipelineByPropertyName )]
        [switch]
        $AsOrg,

        # Use the 'Component' template and set the parent to ParentOrg
        [Parameter( ParameterSetName = "AsComponent", Mandatory, ValueFromPipelineByPropertyName )]
        [switch]
        $AsComponent,

        # The Containing Org for component types.
        [Parameter( ParameterSetName = "AsComponent", Mandatory, ValueFromPipelineByPropertyName )]
        [ArgumentCompleter( {
            param ( $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters )
            (get-rbacOrg -org "$wordToComplete*").Org
        })]
        [String]$ParentOrg,

        # Clear and recreate any memberships for default roles. This will effectively re-align the roles with the spec and should be low-impact
        [Switch]$ResetRoleMembership,

        # Clear and recreate the memberlist of rights. Currently should only remove rights/roles groups (by name)
        [Switch]$ResetRightsMembership,

        [Microsoft.ActiveDirectory.Management.ADDirectoryServer]$Server = (get-addomainController -Writable -Discover)
    )

    BEGIN {
        $shouldProcess = @{
            Confirm = [bool]($ConfirmPreference -eq "low")
            Whatif  = [bool]($WhatIfPreference.IsPresent)
            verbose = [bool]($VerbosePreference -ne "SilentlyContinue")
        }
    }
    PROCESS {

        write-loghandler -message "Starting to Add structure from template (DC: $($Server.Hostname))" -level "verbose"
        <#
        # Is this actually needed????
            if ($PsItem.Name) { $Name = $_.Name }
            if ($PsItem.Description) { $Description = $_.Description }
            if ($PsItem.Path) { $Path = $_.Path }
            if ($PsItem.Template) { $Template = $_.Template }
            if ($PsItem.ParentOrg) { $ParentOrg = $_.ParentOrg }
        #>

        if ($PSItem) {
            if ($_.name -ne $name -or $_.description -ne $description -or $_.path -ne $path -or $_.parentOrg -ne $ParentOrg) {
                write-loghandler -level "warning" -message "This shouldnt happen, uncomment lines 65+ in add-oustructurefromtemplate...."
                throw "Whoops"
            }
        }

        if ($AsGlobal -or $AsOrg -or $AsComponent) {
            switch ($true) {
                $Asglobal {
                    $template = $GlobalTemplate
                    $MockObject = get-RBACGlobal -mock -Description $Description -Detailed
                    $Name = $(split-LDAPPath -distinguishedName $Settings['OUPaths']['Global'] -leaf -nodeNameOnly)
                    $GPOParam = @{
                        Global = $true
                    }
                }
                $AsOrg {
                    $Template = $OrgTemplate
                    $MockObject = get-RBACOrg -org $Name  -mock -Description $Description -Detailed
                    ## TODO
                    $GPOParam = @{
                        Org = $Name
                    }
                }
                $AsComponent {
                    $template = $ComponentTemplate
                    $MockObject = get-RBACComponent -component $Name -mock -Description $Description -oRG $parentOrg -Detailed
                    $GPOParam = @{
                        Org = $ParentOrg
                        Component = $name
                    }
                    $mockObject.Parents.remove("Global")
                }
            }
        } else {
            $mockObject = @{
                Name = $name
                Path = $Path
                DistinguishedName = "OU=$name,$path"
                Children = ($Template['LDAPContainers'] | foreach-object {
                    [pscustomobject]$_ | resolve-rbacchildren -baseDN "OU=$name,$path"
                })
                Parents = @()
                ObjectMidName = $name
            }
            $GPOParam = $false
        }
        $Path = $MockObject.Path
        $GroupMidName = $MockObject.ObjectMidName
        $RightsPath = $MockObject.Children.$($settings.Names.RightsOU).distinguishedName
        $RolesPath = $MockObject.Children.$($settings.Names.RolesOU).distinguishedName
        $parentOrgObj = $MockObject.Parents

        $resetRoleParam = @{
            resetMembership = [bool]($ResetRoleMembership)
        }
        $resetRightsParam = @{
            resetMembers = [bool]($ResetRightsMembership)
        }

        Write-loghandler -level "info" -message  "Starting OU Structure processing"
        $MockObject.Children.getEnumerator().values |  createOrSetOU -server $server @shouldProcess | format-Table

        Write-loghandler -level "info" -message "Finished OU Structure processing"
        if ($Template['DefaultRights']) {
            Write-loghandler -level "info" -message  ("{0,-48} @ {1}" -f "About to create default rights", $RightsPath)
        }
        $rightsDef = [hashtable]::new()
        foreach ($Group in $Template['DefaultRights']) {
            $def = [pscustomobject]@{
                Name        = "{0}-{1}-{2}" -f $Settings['Names']['RightsName'],$GroupMidName,$Group.nameSuffix
                Description = $Group.Description
                path        = $RightsPath
                GroupScope  = $settings.AppSettings.RightScope
                Info        = $Group.Description
                Members     = @()
                memberOf    = @()
            }

            if ($Group.AddParents -ne $false -and $ParentOrgObj.count -gt 0) {
                foreach ($parent in $parentOrgObj.getEnumerator()) {
                    write-loghandler -level "warning" -message "Adding parents for: $($def.name) (Parent: $($parent.value.name))" -target $def.name
                    try {
                        $parentGroupFilter = "{0} -eq '{1}{2}-{3}-{4}' -or {0} -eq '{2}-{3}-{4}' -and GroupScope -eq '{5}'" -f "name", $Settings.Names.RightsPrefix, $Settings.Names.RightsName, $parent.value.ObjectMidName, $Group.nameSuffix, $Settings.AppSettings.RightScope
                        $parentGroupName = (get-adgroup -filter $parentGroupFilter -searchBase $parent.Value.children.Rights.DistinguishedName -server $Server ).name
                        if ($ParentGroupName.count -gt 1) {
                            write-loghandler -level "warning" -message "Multiple groups found, assuming group with rightPrefix"
                            $parentGroupName = @($parentGroupName.where({ $_ -like "$RightPrefix*" }))
                        }
                        if ($ParentGroupName.count -gt 1) {
                            throw "Still too many groups! $parentGroupFilter"
                        }
                        write-loghandler -level "Verbose" -message "Adding the parent Org group $ParentGroupName as a member to $($def.name)"
                        $def.members = $parentGroupName
                    }
                    catch {
                        write-loghandler -level "warning" -message $_.exception.getType().fullname
                        write-loghandler -level "warning" -message "Parent org group doesn't exist? skipping"
                    }
                }
            }
            if (-not $group.DoNotPrefixGroupName) {
                $def.name = "{0}{1}" -f $Settings['Names']['RightsPrefix'],$def.name
            }

            #Write-loghandler -level "info" -message (" {0,-30}" -f $def.name, $def.path) -suppressCaller -suppressTimestamp
            $rightsDef.$($group.nameSuffix) = $def
        }

        $rightsDef.values  | CreateOrSetGroup @ShouldProcess @resetRightsParam | format-table

        if ($Template['DefaultRoles']) {
            write-loghandler -level "Info" -message "Starting Roles processing at $rolesPath"
        }
        $rolesDef = [hashtable]::new()
        foreach ($Group in $Template['DefaultRoles']) {
            try {
                write-loghandler -level "Debug" -message $Group.nameSuffix
            } catch {
                write-warning "Wh"
            }
            $def = [psCustomObject]@{
                Name        = "{0}-{1}-{2}{3}" -f $Settings['Names']['RolesName'], $GroupMidName, $Group.nameSuffix, $Settings.Names.RolesSuffix
                Description = $Group.Description
                path        = $RolesPath
                GroupScope  = $settings.AppSettings.RoleScope
                Info        = $Group.Description
                MemberOf    = @()
            }
            write-loghandler -level "Debug" -message ("{0,-32} {1}" -f $def.name, $def.Description)
            if ($Group.AddParents -eq $true) {
                $parentGroupFilter = "{0} -eq '{2}-{3}-{4}{1}' -or {0} -eq '{2}-{3}-{4}' -and GroupScope -eq '$RoleScope'" -f "name", $Settings.Names.RolesSuffix, $Settings.Names.RightsName, $ParentPrincipalPrefix, $Group.nameSuffix, $RoleScope
                $parentGroupName = "{0}-{1}-{2}" -f $Settings['Names']['RolesName'], $ParentPrincipalPrefix, $Group.nameSuffix
                $parentGroupName = (get-adgroup -filter $parentGroupFilter -searchBase $parentOrgObj.distinguishedName -server $Server ).name
                if ($ParentGroupName.count -gt 1) {
                    write-loghandler -level "warning" -message "Multiple groups found, assuming group with rightPrefix"
                    $parentGroupName = @($parentGroupName.where({ $_ -like "$RightPrefix*" }))
                }
                if ($ParentGroupName.count -gt 1) {
                    throw "Still too many groups! $parentGroupFilter"
                }
                write-loghandler -level "Verbose" -message "Add parent group as member: $parentGroupName"
                $def.members = $ParentGroupName
            }

            $protectedRole = $false
            $def.memberOf = $Group.rights | foreach-object {
                if (-not $rightsDef.ContainsKey($_)) {
                    throw "Missing rights definition for role: $($def.name): $_"
                }
                $rightName = $rightsDef.$_.name
                if ($rightName -like "*$($Settings.Names.RightsProtected)*") {
                    write-loghandler -level "" -message "âš¡âš¡âš¡$rightNameâš¡âš¡âš¡"
                    $protectedRole = $true
                }
                $rightName
            }
            if ($group.auxiliaryGroups) {
                $def.memberof += $Group.auxiliaryGroups
            }
            if ($true -eq $Group.Protected) {
                $def.memberOf += "Protected Users"
                $protectedRole = $true
            }
            if ($protectedRole) {
                $def.name = "{0}{1}" -f $settings.Names.RoleProtected, $def.name
            }
            write-loghandler -level "Info" -message ("{0,-32} {1}" -f $def.name, $def.Description)
            foreach ($g in $def.memberOf) {
                if ($null -ne $g) {
                    write-logHandler -level "Info" -message $g -indentlevel 1 -suppressTimestamp -suppressCaller
                }
            }

            $rolesDef.$($Group.nameSuffix) = $def
        }
        $rolesDef.values | CreateOrSetGroup @shouldProcess @resetRoleParam | out-null

        write-loghandler -level "Info" -message "Finished processing roles.`r`n`r`n"

        # Pre-create the Deny permissions as it's relatively slow to do.
        $ObjectGUIDs = get-ADObjectGUIDs
        $newOUPermsDef = ${function:new-OUPermission}.toString()
        $DefaultDenyRules = $Settings['AppSettings']['DefaultDenyObjectTypes'] | foreach-object {
            [PSCustomObject]@{
                ADRight         = "CreateChild"
                Action          = "Deny"
                TargetObject    = $PSItem
                InheritanceType = "None"
            }
            [PSCustomObject]@{
                ADRight         = "CreateChild"
                Action          = "Deny"
                TargetObject    = $PSItem
                InheritanceType = "All"
            }
        } | new-OUPermission -principal "NT Authority\Everyone" -ObjectGUIDs $ObjectGUIDs

        # Create the default ACL
        $DefaultOU_SDDL = (get-adobject -filter { (ldapDisplayName -eq "organizationalUnit") } -searchBase ($(get-adrootdse).SchemaNamingContext) -properties defaultSecurityDescriptor).defaultSecurityDescriptor
        $defaultOU_ACL = [System.DirectoryServices.ActiveDirectorySecurity]::new()
        $defaultOU_ACL.SetSecurityDescriptorSddlForm($defaultOU_SDDL)

        write-loghandler -level "Debug" -message "Starting OUDelegations..."
        $logHandlerDef = ${function:write-loghandler}.toString()
        $newOUPermsDef = ${function:new-OUPermission}.toString()
        $getouacls = ${function:get-ouacls}.toString()
        $addouperms = ${function:add-oupermissions}.toString()
        $getADObjectGUIDs = ${function:get-ADObjectGUIDs}.toString()
        # Build ParameterList
        $Output = $Template['OUDelegations'] | foreach-object -parallel {

            # Bring in needed functions and variables
            $delegation = $_
            $thisElementPath = $using:mockObject.DistinguishedName
            $rightsDef = $using:rightsdef
            $sleepLength = $using:Settings.AppSettings.SleepLength
            $SleepTimeout = $using:Settings.AppSettings.SleepTimeout
            $objectGUIDs = $using:ObjectGUIDs
            ${function:write-loghandler} = $using:loghandlerDef
            ${function:new-OUPermission} = $using:newOUPermsDef
            ${function:add-oupermissions} = $using:addouperms
            ${function:get-ouacls} = $using:getouacls
            ${function:get-ADObjectGUIDs} = $using:getADObjectGUIDs
            $defaultOU_ACL = $using:defaultOU_ACL
            $ShouldProcess = $using:shouldProcess
            import-module -name ActiveDirectory
            $hostWidth = @{hostWidth  = $using:host.ui.rawui.windowsize.width}
            $DebugPreference = $using:debugPreference
            $VerbosePreference = $using:verbosePreference

            $ADPath = $null
            $ADPath = if ($delegation.ADPath) {
                write-loghandler -level "Debug" -message "Using ADPath: $($delegation.ADPath)"
                $delegation.ADPath
            }
            elseif ($delegation.ADPathQuery) {
                write-loghandler -level "Debug" -message "Querying AD with: $($delegation.ADPathQuery)"
                $query = $delegation.ADPathQuery
                write-host "--> Children of $($query.searchbase)"
                (get-adobject @query -server $using:server).DistinguishedName
            }
            elseif ($delegation.ADPathLeafOU) {
                write-loghandler -level "Debug" -message "Deriving ADPath from $($delegation.ADPathLeafOU)"
                join-string -inputObject @($delegation.ADPathLeafOU, $thisElementPath) -Separator ","
            }
            else {
                write-loghandler -level "Debug" -message "Using childpath: $thisElementPath"
                $thisElementPath
            }
            write-loghandler -level "Info" -message "$ADPath"

            $sw = [System.Diagnostics.Stopwatch]::startNew()
            $ACEList = @(
                $delegation.ACLs | Foreach-Object {
                if ($PSItem.PrincipalSuffix -and -not $PSItem.Principal) {
                    $principal = $rightsDef[$($PSItem.PrincipalSuffix)].name
                    write-loghandler  -level "debug" -message "Deriving Principal for: $($PSItem.PrincipalSuffix)"
                }
                elseif ($PSItem.Principal) {
                    $Principal = $PSItem.principal
                    write-loghandler -level "debug" -message "Principal: $Principal"
                }
                else {
                    throw "Missing principal or principal suffix"
                }
                for ($i = 0; $i -lt $SleepTimeout / $sleepLength; $i++) {
                    try {
                        $principalSID = [System.Security.Principal.NTAccount]::new($principal).translate([System.security.Principal.SecurityIdentifier])
                        $identity = [System.Security.Principal.IdentityReference] $principalSID
                        break
                    }
                    catch {
                        write-loghandler -level "warning" -message $_.exception.getType().fullname
                        write-loghandler -level "warning" -message "Could not find principal $principal; sleeping for $($sleepLength)"
                        start-sleep -Seconds $sleepLength
                    }
                }
                write-loghandler -level "debug" -message "Finished Deriving identity. Beginning to create ACEs"

                if (-not $identity) {
                    write-error "Failed to resolve principal before timeout: $($PSItem.PrincipalSuffix)"
                    write-loghandler -level "warning" -message "Skipping this set of ACLs."
                    continue
                }

                try {
                    $PSItem.ACEs | foreach-object  {
                        new-oupermission -Identity $identity @_ -objectGUIDs $ObjectGUIDs
                    }
                } catch {
                    write-warning "Whoops"
                    throw $_
                }
            })
            $sw.stop()
            write-loghandler -level "debug" -message "Finished creating $($ACEList.count) DACLs in $($sw.ElapsedMilliseconds) ms"
            $sw.reset()
            if ($delegation.ApplyDefaultDeny -ne $false) {

                # Check the ACE list for 'createChild' rights, and add the associated objects to a list. This lets us affirmatively deny object creation for items not in the list
                $CreateChildACEs = $aceList.where({ $_.ActiveDirectoryRights -Like "*CreateChild*" })
                $AllowCreationObjects = $CreateChildACEs | foreach-object {
                    $GUIDList = @($_.objectType.guid, $_.InheritedObjectType.guid).where({ $_ -ne "00000000-0000-0000-0000-000000000000" })
                    $objectGUIDs.where({ $_.GUID -in $GUIDList })
                }
                if ($AllowCreationObjects.count -gt 0) {
                    write-loghandler -level "Verbose" -message "CreateChild found, only allowing the following child items: $($AllowCreationObjects.name -join "; ")"
                }
                elseif ($createChildAces.count -gt 0) {
                    Write-error "Whoops, we have createChild ACEs but our filter failed???"
                }
                if (-not $delegation.DefaultDenyInheritance) {
                    $denyInheritance = "None"
                }
                else {
                    $denyInheritance = $delegation.DefaultDenyInheritance
                }
                # DefaultDenyRules should have an 'inheritance all' and an 'inheritance none' ace for each object, so we're just filtering down.
                $DenyRules = $DefaultDenyRules | where-object { $_.ObjectType -notin $AllowCreationObjects.GUID -and $_.inheritanceType -eq $denyInheritance }
                $ACEList += $DenyRules
                write-loghandler -level "debug" -message "Adding $($denyRules.count) Deny rules."
            }

            if ($Acelist.count -gt 0) {
                Add-OUPermissions -path $ADPath -aceList $AceList @shouldProcess -rebuild -defaultACL $defaultOU_ACL
            }

        }

        if ($GPOParam) {
            $GPOSpecList = foreach ($GPO in $Template['GPOs']) {
                    [pscustomObject]@{
                        GPOTemplate = $GPO
                    }
                }
                if ($GPOSpecList) {
                    if($PSCmdlet.ShouldProcess("Creating GPOs")) {
                        $GPOSpecList | CreateOrSetGPO @GPOParam -server $Server
                    }
                }
            } else {
                write-loghandler -level "warning" -message "GPO behavior is undefined here. Please report this as a bug."
            }
    }


    <#if ($ACEList) { Write-Host "`r`nApplying AD PSItems" }
        $textIndent = "|-->"
        $ACLList | group-object path | foreach-object {
            Write-Host ("--->{0,-40}" -f $_.name)
            $_.group.acl | foreach-object {
                write-loghandler -level "Verbose" -message ("{0,6}{1,-48} on: {2,-36} IOT: {3,-36} {4}" -f $textIndent, $_.Principal, ($_.ExtendedRight + $_.TargetObject), $_.AppliesTo, $_.ADRight)
                #Add-OUPermission @_
            }
            write-host ""
        }#>

}