Public/New-LapsObject.ps1
Function New-LAPSobject { <# .SYNOPSIS Configures and manages Local Administrator Password Solution (LAPS) objects and delegations in Active Directory. .DESCRIPTION This function provides comprehensive LAPS configuration and management capabilities: Key Features: - Extends AD schema for LAPS if not already configured - Creates and configures LAPS delegations across all infrastructure tiers - Sets up tiered PAW (Privileged Access Workstation) LAPS permissions - Implements site-specific LAPS delegations - Supports bulk operations for multiple OUs - Provides detailed logging and error handling The function follows Microsoft's tiered administration model: - Tier 0: Domain Controllers and critical infrastructure - Tier 1: Member servers and infrastructure services - Tier 2: User workstations and devices Prerequisites: - Active Directory PowerShell module - LAPS PowerShell module - Schema Admin rights (for initial setup) - Enterprise Admin rights (for delegation setup) - Valid configuration XML file Configuration Requirements: The XML file must contain: - Naming conventions for groups and OUs - Tier definitions and delegations - Site-specific configurations - Security group mappings .PARAMETER ConfigXMLFile [System.IO.FileInfo] Full path to the configuration XML file containing LAPS settings. The XML file must include: - Group naming conventions - OU structure definitions - Security principal mappings - Tier-specific configurations Default value: 'C:\PsScripts\Config.xml' Validation: - Must exist and be accessible - Must contain valid XML structure - Must include required configuration elements .EXAMPLE New-LAPSobject -ConfigXMLFile 'C:\Config\Enterprise.xml' -Verbose Description: Configures LAPS using production configuration file: 1. Validates XML configuration 2. Extends schema if needed 3. Creates tier-specific delegations 4. Sets up PAW permissions 5. Configures site-level access .EXAMPLE $params = @{ ConfigXMLFile = 'D:\Scripts\Config.xml' } New-LAPSobject @params -WhatIf Shows what changes would be made using specified config file. .OUTPUTS [void] This function does not generate any output. Use -Verbose for detailed progress information. .NOTES Used Functions: Name ║ Module/Namespace ═══════════════════════════════════════╬════════════════════════ Import-MyModule ║ EguibarIT Get-FunctionDisplay ║ EguibarIT Set-AdAclLaps ║ EguibarIT.DelegationPS Get-ADGroup ║ ActiveDirectory Get-ADOrganizationalUnit ║ ActiveDirectory Add-ADGroupMember ║ ActiveDirectory Remove-ADGroupMember ║ ActiveDirectory Update-LapsADSchema ║ LAPS New-Variable ║ Microsoft.PowerShell.Utility Write-Verbose ║ Microsoft.PowerShell.Utility Write-Debug ║ Microsoft.PowerShell.Utility Write-Error ║ Microsoft.PowerShell.Utility Test-Path ║ Microsoft.PowerShell.Management Get-Content ║ Microsoft.PowerShell.Management Get-Variable ║ Microsoft.PowerShell.Utility .NOTES Version: 1.2 DateModified: 31/Mar/2024 LasModifiedBy: Vicente Rodriguez Eguibar vicente@eguibar.com Eguibar IT http://www.eguibarit.com .LINK https://github.com/vreguibar/EguibarIT .LINK https://techcommunity.microsoft.com/t5/core-infrastructure-and-security/local-administrator-password-solution-laps-implementation-hints-and/ba-p/258019 .LINK https://learn.microsoft.com/en-us/windows-server/identity/laps/laps-overview .LINK https://learn.microsoft.com/en-us/windows-server/identity/securing-privileged-access/securing-privileged-access-reference-material #> [CmdletBinding( SupportsShouldProcess = $true, ConfirmImpact = 'Medium' )] [OutputType([void])] Param ( # PARAM1 full path to the configuration.xml file [Parameter(Mandatory = $true, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True, ValueFromRemainingArguments = $false, HelpMessage = 'Full path to the configuration.xml file', Position = 0)] [ValidateScript( { Test-Path $_ }, ErrorMessage = 'Config file not found or not accessible: {0}' )] [PSDefaultValue(Help = 'Default Value is "C:\PsScripts\Config.xml"')] [System.IO.FileInfo] $ConfigXMLFile = 'C:\PsScripts\Config.xml' ) Begin { Set-StrictMode -Version Latest # Initialize logging if ($null -ne $Variables -and $null -ne $Variables.Header) { $txt = ($Variables.Header -f (Get-Date).ToString('dd/MMM/yyyy'), $MyInvocation.Mycommand, (Get-FunctionDisplay -HashTable $PsBoundParameters -Verbose:$False) ) Write-Verbose -Message $txt } #end If ############################## # Module imports Import-MyModule -Name 'ActiveDirectory' -Verbose:$false Import-MyModule -Name 'EguibarIT.DelegationPS' -Verbose:$false Import-MyModule -Name 'LAPS' -Verbose:$false ############################## # Variables Definition [hashtable]$Splat = [hashtable]::New([StringComparer]::OrdinalIgnoreCase) try { # Check if Config.xml file is loaded. If not, proceed to load it. If (-Not (Test-Path -Path variable:confXML)) { # Check if the Config.xml file exist on the given path If (Test-Path -Path $PSBoundParameters['ConfigXMLFile']) { #Open the configuration XML file $confXML = [xml](Get-Content $PSBoundParameters['ConfigXMLFile']) Write-Debug -Message 'Successfully loaded configuration XML' } #end if } #end if } catch { Write-Error -Message 'Error when reading XML file' throw } If (-Not (Test-Path -Path variable:NC)) { # Naming conventions hashtable $NC = @{'sl' = $confXML.n.NC.LocalDomainGroupPreffix 'sg' = $confXML.n.NC.GlobalGroupPreffix 'su' = $confXML.n.NC.UniversalGroupPreffix 'Delim' = $confXML.n.NC.Delimiter 'T0' = $confXML.n.NC.AdminAccSufix0 'T1' = $confXML.n.NC.AdminAccSufix1 'T2' = $confXML.n.NC.AdminAccSufix2 } } #('{0}{1}{2}{1}{3}' -f $NC['sg'], $NC['Delim'], $confXML.n.Admin.lg.PAWM, $NC['T0']) # SG_PAWM_T0 $securityGroups = @{ 'SL_InfraRight' = '{0}{1}{2}' -f $NC['sl'], $NC['Delim'], $confXML.n.Admin.LG.InfraRight.Name 'SL_PISM' = '{0}{1}{2}' -f $NC['sl'], $NC['Delim'], $confXML.n.Admin.LG.PISM.Name 'SL_PAWM' = '{0}{1}{2}' -f $NC['sl'], $NC['Delim'], $confXML.n.Admin.LG.PAWM.Name 'SL_SvrAdmRight' = '{0}{1}{2}' -f $NC['sl'], $NC['Delim'], $confXML.n.Servers.LG.SvrAdmRight.Name } foreach ($group in $securityGroups.GetEnumerator()) { if (-not (Test-Path -Path variable:$($group.Key))) { New-Variable -Name $group.Key -Value (Get-ADGroup -Identity $group.Value) -ErrorAction Stop } #end If } #end Foreach # Organizational Units Distinguished Names # IT Admin OU If (-Not (Test-Path -Path variable:ItAdminOu)) { $ItAdminOu = $confXML.n.Admin.OUs.ItAdminOU.name } # IT Admin OU Distinguished Name If (-Not (Test-Path -Path variable:ItAdminOuDn)) { New-Variable -Name 'ItAdminOuDn' -Value ('OU={0},{1}' -f $ItAdminOu, $Variables.AdDn) -Option ReadOnly -Force } # Servers OU If (-Not (Test-Path -Path variable:ServersOu)) { $ServersOu = $confXML.n.Servers.OUs.ServersOU.name } # Servers OU Distinguished Name If (-Not (Test-Path -Path variable:ServersOuDn)) { $ServersOuDn = 'OU={0},{1}' -f $ServersOu, $Variables.AdDn } # It InfraServers OU $ItInfraServersOu = $confXML.n.Admin.OUs.ItInfraOU.name # It PAW OU Distinguished Name $ItInfraServersOuDn = 'OU={0},{1}' -f $ItInfraServersOu, $ItAdminOuDn # It InfraServers Tier0 OU $ItInfraT0OU = $confXML.n.Admin.OUs.ItInfraT0OU.name # It InfraServers Tier0 OU Distinguished Name $ItInfraT0OUDN = 'OU={0},{1}' -f $ItInfraT0OU, $ItInfraServersOuDn # It InfraServers Tier1 OU $ItInfraT1OU = $confXML.n.Admin.OUs.ItInfraT1OU.name # It InfraServers Tier1 OU Distinguished Name $ItInfraT1OUDN = 'OU={0},{1}' -f $ItInfraT1OU, $ItInfraServersOuDn # It InfraServers Tier2 OU $ItInfraT2OU = $confXML.n.Admin.OUs.ItInfraT2OU.name # It InfraServers Tier2 OU Distinguished Name $ItInfraT2OUDN = 'OU={0},{1}' -f $ItInfraT2OU, $ItInfraServersOuDn # It InfraServers Staging Tier0 OU $ItInfraStagingOU = $confXML.n.Admin.OUs.ItInfraStagingOU.name # It InfraServers Staging Tier0 OU Distinguished Name $ItInfraStagingOUDN = 'OU={0},{1}' -f $ItInfraStagingOU, $ItInfraServersOuDn # It PAW OU $ItPawOu = $confXML.n.Admin.OUs.ItPawOU.name # It PAW OU Distinguished Name $ItPawOuDn = 'OU={0},{1}' -f $ItPawOu, $ItAdminOuDn # It PAW Tier0 OU $ItPawT0OU = $confXML.n.Admin.OUs.ItPawT0OU.name # It PAW Tier0 OU Distinguished Name $ItPawT0OUDN = 'OU={0},{1}' -f $ItPawT0OU, $ItPawOuDn # It PAW Tier1 OU $ItPawT1OU = $confXML.n.Admin.OUs.ItPawT1OU.name # It PAW Tier1 OU Distinguished Name $ItPawT1OUDN = 'OU={0},{1}' -f $ItPawT1OU, $ItPawOuDn # It PAW Tier2 OU $ItPawT2OU = $confXML.n.Admin.OUs.ItPawT2OU.name # It PAW Tier2 OU Distinguished Name $ItPawT2OUDN = 'OU={0},{1}' -f $ItPawT2OU, $ItPawOuDn # It PAW Staging Tier0 OU $ItPawStagingOU = $confXML.n.Admin.OUs.ItPawStagingOU.name # It PAW Tier2 OU Distinguished Name $ItPawStagingOUDN = 'OU={0},{1}' -f $ItPawStagingOU, $ItPawOuDn # Sites OU $SitesOu = $confXML.n.Sites.OUs.SitesOU.name # Sites OU Distinguished Name $SitesOuDn = 'OU={0},{1}' -f $SitesOu, $Variables.AdDn #endregion Declarations # Check if schema is extended for LAPS. Extend it if not. Write-Debug -Message 'Checking LAPS schema configuration' Try { if ($null -eq $Variables.GuidMap['msLAPS-Password']) { if ($PSCmdlet.ShouldProcess('AD Schema', 'Extend for LAPS')) { Write-Verbose -Message ' LAPS is NOT supported on this environment. Proceeding to configure it by extending the Schema.' # Temporarily add to Schema Admins if needed $isSchemaAdmin = (Get-ADUser $env:UserName -Properties memberof).memberof -like 'CN=Schema Admins*' if (-not $isSchemaAdmin) { Write-Verbose -Message 'Member is not a Schema Admin... adding it.' Add-ADGroupMember -Identity 'Schema Admins' -Members $env:username }#end if # Modify Schema try { Write-Verbose -Message 'Extending AD schema for LAPS...!' Update-LapsADSchema -Confirm:$false -Verbose } catch { Write-Error -Message ('Failed to extend schema: {0}' -f $_.Exception.Message) throw } finally { # If Schema extension OK, remove user from Schema Admin if (-not $isSchemaAdmin) { Remove-ADGroupMember -Identity 'Schema Admins' -Members $env:username -Confirm:$false } } #end Try-Catch-Finally }#end if }#end if }#end try catch { Write-Error -Message 'Error when trying to update LAPS schema' throw } Finally { Write-Verbose -Message 'Schema was extended successfully for LAPS.' }#end finally } #end Begin Process { # Make Infrastructure Servers modifications $Splat = @{ ResetGroup = $SL_PISM.SamAccountName ReadGroup = $SL_InfraRight.SamAccountName } Set-AdAclLaps @Splat -LDAPpath $ItInfraT0OUDN Set-AdAclLaps @Splat -LDAPpath $ItInfraT1OUDN Set-AdAclLaps @Splat -LDAPpath $ItInfraT2OUDN Set-AdAclLaps @Splat -LDAPpath $ItInfraStagingOUDN # Make PAW modifications $Splat = @{ ResetGroup = $SL_PAWM.SamAccountName ReadGroup = $SL_InfraRight.SamAccountName } Set-AdAclLaps @Splat -LDAPpath $ItPawT0OUDN Set-AdAclLaps @Splat -LDAPpath $ItPawT1OUDN Set-AdAclLaps @Splat -LDAPpath $ItPawT2OUDN Set-AdAclLaps @Splat -LDAPpath $ItPawStagingOUDN # Make Servers Modifications Set-AdAclLaps -ResetGroup $SL_SvrAdmRight.SamAccountName -ReadGroup $SL_SvrAdmRight.SamAccountName -LDAPpath $ServersOuDn # Make Sites Modifications # Get the DN of 1st level OU underneath SERVERS area $Splat = @{ Filter = '*' SearchBase = $SitesOuDn SearchScope = 'OneLevel' } $AllSubOu = Get-ADOrganizationalUnit @Splat | Select-Object -ExpandProperty DistinguishedName # Iterate through each sub OU and invoke delegation Foreach ($Item in $AllSubOu) { # Exclude _Global OU from delegation If (-not($item.Split(',')[0].Substring(3) -eq $confXML.n.Sites.OUs.OuSiteGlobal.name)) { # Get group who manages Desktops and Laptops $Id = ('{0}{1}{2}{1}{3}' -f $NC['sl'], $NC['Delim'], $confXML.n.Sites.LG.PcRight.Name, ($item.Split(',')[0].Substring(3)) ) $CurrentGroup = (Get-ADGroup -Identity $Id).SamAccountName # Desktops $Splat = @{ ResetGroup = $CurrentGroup.SamAccountName ReadGroup = $CurrentGroup.SamAccountName LDAPpath = 'OU={0},{1}' -f $confXML.n.Sites.OUs.OuSiteComputer.Name, $Item } Set-AdAclLaps @Splat # Laptop $Splat = @{ ResetGroup = $CurrentGroup.SamAccountName ReadGroup = $CurrentGroup.SamAccountName LDAPpath = 'OU={0},{1}' -f $confXML.n.Sites.OUs.OuSiteLaptop.Name, $Item } Set-AdAclLaps @Splat } }#end foreach } #end Process End { if ($null -ne $Variables -and $null -ne $Variables.Footer) { $txt = ($Variables.Footer -f $MyInvocation.InvocationName, 'creating LAPS and Delegations.' ) Write-Verbose -Message $txt } #end If } #end End } #end Function New-LapsObject |