From 030730448dc8fcc45ef5a833116b59ecb8bdaf8b Mon Sep 17 00:00:00 2001 From: Corbin Date: Sat, 18 Apr 2026 08:37:01 -0400 Subject: [PATCH] WIP wrapper module for PnP PowerShell SharePoint permissions --- m365/PnPFolderAcl/PnPFolderAcl.psm1 | 234 ++++++++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 m365/PnPFolderAcl/PnPFolderAcl.psm1 diff --git a/m365/PnPFolderAcl/PnPFolderAcl.psm1 b/m365/PnPFolderAcl/PnPFolderAcl.psm1 new file mode 100644 index 0000000..105852f --- /dev/null +++ b/m365/PnPFolderAcl/PnPFolderAcl.psm1 @@ -0,0 +1,234 @@ + +#region Private Helpers + +<# +.SYNOPSIS + Tests if a PnP Group exists. +.DESCRIPTION + This function tests if a PnP Group exists in the current SharePoint site. +.PARAMETER Identity + The identity of the group to test. +.EXAMPLE + Test-PnPGroup -Identity "MyGroup" +#> +function Test-PnPGroup { + param( + [string]$Identity + ) + + try { + Get-PnPGroup -Identity $Identity -ErrorAction Stop | Out-Null + return $true + } catch { + return $false + } +} + +<# +.SYNOPSIS + Tests if an Entra ID Group exists and PnP can resolve it. +.DESCRIPTION + This function tests if a Entra ID Group exists and can be resolved by PnP. +.PARAMETER Identity + The identity of the group to test. +.EXAMPLE + Test-EntraIdGroup -Identity "MyGroup" +#> +function Test-EntraIdGroup { + param( + [string]$Identity + ) + + try { + Get-PnPEntraIdGroup -Identity $Identity -ErrorAction Stop | Out-Null + return $true + } catch { + return $false + } +} + +#endregion + +#region Public Functions + +<# +.SYNOPSIS + Sets permissions on a folder in a SharePoint document library. +.DESCRIPTION + This script breaks permission inheritance on a specified folder in a SharePoint document library and assigns permissions to a specified owner group and additional groups defined in the ACL parameter. +.PARAMETER Name + The name of the folder to set permissions on. +.PARAMETER List + The name of the document library containing the folder. Default is 'Shared Documents'. +.PARAMETER Owner + The name of the SharePoint group to assign as the owner of the folder with 'Full Control' permissions. +.PARAMETER Acl + An array of objects defining additional groups and their permissions to assign to the folder. Each object should have a 'DisplayName' property for the group name and a 'Role' property for the permission level (e.g., 'Read', 'Edit'). +.EXAMPLE + $Acl = @( + @{ DisplayName = "SG-ADMIN-AdvocateFloats-Dynamic"; Role = "Edit" }, + @{ DisplayName = "SG-ADMIN-AdvocateManagers-Dynamic"; Role = "Edit" } + ) + .\Set-PnPFolderAcl.ps1 -Name "ProjectX" -List "Shared Documents" -Owner "Project Owners" -Acl $Acl +#> +function Set-PnPFolderAcl { + [CmdletBinding( + SupportsShouldProcess = $true, + ConfirmImpact = 'High' + )] + param( + [Parameter( + Mandatory, + ValueFromPipeline, + ValueFromPipelineByPropertyName + )] + [string]$FolderName, + + [Parameter()] + [string]$List = 'Shared Documents', + + [Parameter(Mandatory)] + [string]$Owner, + + [Parameter(Mandatory)] + [pscustomobject[]]$Acl + ) + + begin { + if (-not (Get-PnPContext)) { + Throw "Not connected to a SharePoint site. Run Connect-PnPOnline first." + } + + if (-not (Get-PnPList -Identity $List -ErrorAction SilentlyContinue)) { + Throw "The specified list '$List' does not exist." + } + + if (-not (Test-PnPGroup -Identity $Owner)) { + Throw "The specified owner group '$Owner' does not exist." + } + + $ValidRoles = Get-PnPRoleDefinition | Select-Object -ExpandProperty Name + + if (-not $ValidRoles) { + throw "Unable to retrieve SharePoint role definitions." + } + + + foreach ($entry in $Acl) { + if (-not ($entry.PSObject.Properties.Name -contains 'Group' -and + $entry.PSObject.Properties.Name -contains 'Role')) { + Throw "Each ACL entry must contain 'Group' and 'Role' properties." + } + if (-not (Test-PnPGroup -Identity $entry.Group) -and + -not (Test-EntraIdGroup -Identity $entry.Group)) { + Throw "The specified group '$($entry.Group)' does not exist as a PnP Group or Entra ID Group." + } + if ($entry.Role -notin $ValidRoles) { + Throw "Invalid role '$($entry.Role)' specified for group '$($entry.Group)'. Valid roles are: $($ValidRoles -join ', ')." + } + } + } + + process { + $FolderPath = "$List/$FolderName" + + # Retrieve folder and current permissions + $FolderItem = Get-PnPFolder -Url $FolderPath -ErrorAction Stop + $ListItem = Get-PnPListItem -List $List -Id $FolderItem.ListItemAllFields.Id + + + # Break inheritance ONLY IF the folder doesn't already have unique permissions + if (-not $ListItem.HasUniqueRoleAssignments) { + + if ($PSCmdlet.ShouldProcess( + $FolderPath, + "Break inheritance and grant Full Control to '$Owner' on '$FolderPath'." + )) { + + Write-Verbose "Breaking inheritance for '$FolderPath'." + Write-Verbose "Granting 'Full Control' to '$Owner' on '$FolderPath'." + + Set-PnPFolderPermission ` + -List $List ` + -Identity $FolderPath ` + -Group $Owner ` + -AddRole 'Full Control' ` + -ClearExisting + } + } + + # Get existing role assignments + $RoleAssignments = Get-PnPProperty -ClientObject $ListItem -Property RoleAssignments + + $CurrentAcl = foreach ($ra in $RoleAssignments) { + $principal = Get-PnPProperty -ClientObject $ra -Property Member + $bindings = Get-PnPProperty -ClientObject $ra -Property RoleDefinitionBindings + + foreach ($binding in $bindings) { + [pscustomobject]@{ + Principal = $principal.Title + Role = $binding.Name + } + } + } + + # foreach ($entry in $CurrentAcl) { + + # if ($entry.Principal -eq $Owner) { + # continue + # } + + # if + # } + + foreach ($entry in $Acl) { + + $GroupName = $entry.Group + $Role = $entry.Role + + $AlreadyAssigned = $ResolvedRoles | Where-Object { + $_.Principal -eq $GroupName -and + $_.Role -eq $Role + } + + if ($AlreadyAssigned) { + Write-Verbose "Permission '$Role' already assigned to '$GroupName' on '$FolderPath'. Skipping." + continue + } + + if (Test-PnPGroup $GroupName) { + if ($PSCmdlet.ShouldProcess( + $FolderPath, + "Grant '$Role' to SharePoint group '$GroupName' on '$FolderPath'." + )) { + + Write-Verbose "Granting '$Role' to SharePoint group '$GroupName' on '$FolderPath'." + + Set-PnPFolderPermission ` + -List $List ` + -Identity $FolderPath ` + -Group $GroupName ` + -AddRole $Role + } + } elseif (Test-EntraIdGroup $GroupName) { + if ($PSCmdlet.ShouldProcess( + $FolderPath, + "Grant '$Role' to Entra ID group '$GroupName' on '$FolderPath'." + )) { + + Write-Verbose "Granting '$Role' to Entra ID group '$GroupName' on '$FolderPath'." + + Set-PnPFolderPermission ` + -List $List ` + -Identity $FolderPath ` + -Group $GroupName ` + -AddRole $Role + } + } + } + } +} + +#endregion + +Export-ModuleMember -Function Set-PnPFolderAcl \ No newline at end of file