Skip to content

Commit 78508a8

Browse files
authored
Merge pull request #1083 from KelvinTegelaar/dev
[pull] dev from KelvinTegelaar:dev
2 parents fb0bfcd + c134795 commit 78508a8

20 files changed

Lines changed: 1193 additions & 128 deletions

Modules/CIPPCore/Public/Add-CIPPGroupMember.ps1

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,29 @@ function Add-CIPPGroupMember {
3434
[string]$APIName = 'Add Group Member'
3535
)
3636
try {
37-
if ($Member -like '*#EXT#*') { $Member = [System.Web.HttpUtility]::UrlEncode($Member) }
3837
$ODataBindString = 'https://graph.microsoft.com/v1.0/directoryObjects/{0}'
39-
$Requests = foreach ($m in $Member) {
38+
$Requests = @(
39+
foreach ($m in $Member) {
40+
if ($m -like '*#EXT#*') { $m = [System.Web.HttpUtility]::UrlEncode($m) }
41+
@{
42+
id = "users-$m"
43+
url = "users/$($m)?`$select=id,userPrincipalName"
44+
method = 'GET'
45+
}
46+
}
4047
@{
41-
id = $m
42-
url = "users/$($m)?`$select=id,userPrincipalName"
48+
id = 'group'
49+
url = "groups/$($GroupId)?`$select=id,displayName"
4350
method = 'GET'
4451
}
45-
}
46-
$Users = New-GraphBulkRequest -Requests @($Requests) -tenantid $TenantFilter
52+
)
53+
$BulkResults = New-GraphBulkRequest -Requests @($Requests) -tenantid $TenantFilter
54+
$Users = @($BulkResults | Where-Object { $_.id -like 'users-*' })
55+
# Group display name for logging; falls back to the id if the lookup failed
56+
# (e.g. the group was addressed by mail rather than GUID).
57+
$GroupName = ($BulkResults | Where-Object { $_.id -eq 'group' }).body.displayName ?? $GroupId
58+
$SuccessfulUsers = [System.Collections.Generic.List[string]]::new()
59+
$FailedUsers = [System.Collections.Generic.List[string]]::new()
4760

4861
if ($GroupType -eq 'Distribution list' -or $GroupType -eq 'Mail-Enabled Security') {
4962
$ExoBulkRequests = [System.Collections.Generic.List[object]]::new()
@@ -58,7 +71,7 @@ function Add-CIPPGroupMember {
5871
}
5972
})
6073
$ExoLogs.Add(@{
61-
message = "Added member $($User.body.userPrincipalName) to $($GroupId) group"
74+
message = "Added member $($User.body.userPrincipalName) to group $($GroupName)"
6275
target = $User.body.userPrincipalName
6376
})
6477
}
@@ -76,6 +89,7 @@ function Add-CIPPGroupMember {
7689
$ExoError = $LastError | Where-Object { $ExoLog.target -in $_.target -and $_.error }
7790
if (!$LastError -or ($LastError.error -and $LastError.target -notcontains $ExoLog.target)) {
7891
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $ExoLog.message -Sev 'Info'
92+
$SuccessfulUsers.Add($ExoLog.target)
7993
}
8094
}
8195
}
@@ -91,25 +105,36 @@ function Add-CIPPGroupMember {
91105
}
92106
}
93107
$AddResults = New-GraphBulkRequest -tenantid $TenantFilter -Requests @($AddRequests)
94-
$SuccessfulUsers = [system.collections.generic.list[string]]::new()
95108
foreach ($Result in $AddResults) {
109+
$UserPrincipalName = ($Users | Where-Object { $_.body.id -eq $Result.id }).body.userPrincipalName
96110
if ($Result.status -lt 200 -or $Result.status -gt 299) {
97-
$FailedUsername = $Users | Where-Object { $_.body.id -eq $Result.id } | Select-Object -ExpandProperty body | Select-Object -ExpandProperty userPrincipalName
98-
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to add member $($FailedUsername): $($Result.body.error.message)" -Sev 'Error'
111+
# Select-Object -First 1: Get-NormalizedError can return multiple strings
112+
# when a message matches more than one of its translation patterns.
113+
$ErrorText = Get-NormalizedError -message ($Result.body.error.message ?? "Request failed with status $($Result.status)") | Select-Object -First 1
114+
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to add member $UserPrincipalName to group $($GroupName): $ErrorText" -Sev 'Error'
115+
$FailedUsers.Add("$UserPrincipalName ($ErrorText)")
99116
} else {
100-
$UserPrincipalName = $Users | Where-Object { $_.body.id -eq $Result.id } | Select-Object -ExpandProperty body | Select-Object -ExpandProperty userPrincipalName
101117
$SuccessfulUsers.Add($UserPrincipalName)
102118
}
103119
}
104120
}
105-
$UserList = ($SuccessfulUsers -join ', ')
106-
$Results = "Successfully added user $UserList to $($GroupId)."
121+
$Messages = [System.Collections.Generic.List[string]]::new()
122+
if ($SuccessfulUsers.Count -gt 0) {
123+
$Messages.Add("Successfully added user $($SuccessfulUsers -join ', ') to group $($GroupName).")
124+
}
125+
if ($FailedUsers.Count -gt 0) {
126+
$Messages.Add("Failed to add $($FailedUsers -join '; ').")
127+
}
128+
$Results = $Messages -join ' '
129+
if ($SuccessfulUsers.Count -eq 0 -and $FailedUsers.Count -gt 0) {
130+
throw $Results
131+
}
107132
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Results -Sev 'Info'
108133
return $Results
109134
} catch {
110135
$ErrorMessage = Get-CippException -Exception $_
111136
$UserList = if ($Users) { ($Users.body.userPrincipalName -join ', ') } else { ($Member -join ', ') }
112-
$Results = "Failed to add user $UserList to $($GroupId) - $($ErrorMessage.NormalizedError)"
137+
$Results = "Failed to add user $UserList to group $($GroupName ?? $GroupId) - $($ErrorMessage.NormalizedError)"
113138
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Results -Sev 'error' -LogData $ErrorMessage
114139
throw $Results
115140
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
function Get-CIPPSPODeletedSites {
2+
<#
3+
.SYNOPSIS
4+
List deleted SharePoint sites (tenant recycle bin) via CSOM
5+
6+
.DESCRIPTION
7+
Retrieves the deleted site collections still restorable from the SharePoint tenant recycle
8+
bin using the CSOM GetDeletedSitePropertiesFromSharePoint method, following the same paging
9+
pattern as Get-CIPPSPOSite.
10+
11+
.PARAMETER TenantFilter
12+
Tenant to query
13+
14+
.EXAMPLE
15+
Get-CIPPSPODeletedSites -TenantFilter 'contoso.onmicrosoft.com'
16+
17+
.FUNCTIONALITY
18+
Internal
19+
#>
20+
[CmdletBinding()]
21+
param(
22+
[Parameter(Mandatory = $true)]
23+
[string]$TenantFilter
24+
)
25+
26+
$SharePointInfo = Get-SharePointAdminLink -Public $false -tenantFilter $TenantFilter
27+
$AdminUrl = $SharePointInfo.AdminUrl
28+
$AdditionalHeaders = @{ 'Accept' = 'application/json;odata=verbose' }
29+
30+
$AllSites = [System.Collections.Generic.List[object]]::new()
31+
$StartIndex = $null
32+
33+
do {
34+
$StartIndexParameter = if ($null -ne $StartIndex) {
35+
"<Parameter Type=`"String`">$([System.Security.SecurityElement]::Escape($StartIndex))</Parameter>"
36+
} else {
37+
'<Parameter Type="Null" />'
38+
}
39+
$XML = @"
40+
<Request AddExpandoFieldTypeSuffix="true" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="SharePoint Online PowerShell (16.0.24908.0)" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009"><Actions><ObjectPath Id="172" ObjectPathId="171" /><ObjectPath Id="174" ObjectPathId="173" /><Query Id="175" ObjectPathId="173"><Query SelectAllProperties="true"><Properties><Property Name="NextStartIndexFromSharePoint" ScalarProperty="true" /></Properties></Query><ChildItemQuery SelectAllProperties="true"><Properties /></ChildItemQuery></Query></Actions><ObjectPaths><Constructor Id="171" TypeId="{268004ae-ef6b-4e9b-8425-127220d84719}" /><Method Id="173" ParentId="171" Name="GetDeletedSitePropertiesFromSharePoint"><Parameters>$StartIndexParameter</Parameters></Method></ObjectPaths></Request>
41+
"@
42+
43+
$Results = New-GraphPostRequest -scope "$AdminUrl/.default" -tenantid $TenantFilter -Uri "$AdminUrl/_vti_bin/client.svc/ProcessQuery" -Type POST -Body $XML -ContentType 'text/xml' -AddedHeaders $AdditionalHeaders
44+
45+
$CsomError = ($Results | Where-Object { $_.ErrorInfo } | Select-Object -First 1).ErrorInfo.ErrorMessage
46+
if ($CsomError) { throw $CsomError }
47+
48+
$SiteCollection = $Results | Where-Object { $_._Child_Items_ }
49+
if ($SiteCollection) {
50+
foreach ($Site in $SiteCollection._Child_Items_) {
51+
[void]$AllSites.Add($Site)
52+
}
53+
$StartIndex = $SiteCollection.NextStartIndexFromSharePoint
54+
} else {
55+
$StartIndex = $null
56+
}
57+
} while ($StartIndex)
58+
59+
return $AllSites
60+
}

Modules/CIPPCore/Public/Get-CIPPSPOSite.ps1

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,44 @@ function Get-CIPPSPOSite {
1010
.PARAMETER TenantFilter
1111
Tenant to query
1212
13+
.PARAMETER SiteUrl
14+
When provided, fetches the properties of this single site (CSOM GetSitePropertiesByUrl)
15+
instead of enumerating every site in the tenant.
16+
1317
.EXAMPLE
1418
Get-CIPPSPOSite -TenantFilter 'contoso.onmicrosoft.com'
1519
20+
.EXAMPLE
21+
Get-CIPPSPOSite -TenantFilter 'contoso.onmicrosoft.com' -SiteUrl 'https://contoso.sharepoint.com/sites/MySite'
22+
1623
.FUNCTIONALITY
1724
Internal
1825
1926
#>
2027
[CmdletBinding()]
2128
param(
2229
[Parameter(Mandatory = $true)]
23-
[string]$TenantFilter
30+
[string]$TenantFilter,
31+
[string]$SiteUrl
2432
)
2533

2634
$SharePointInfo = Get-SharePointAdminLink -Public $false -tenantFilter $TenantFilter
2735
$AdminUrl = $SharePointInfo.AdminUrl
2836

37+
if ($SiteUrl) {
38+
# Single-site fast path: Tenant Constructor -> GetSitePropertiesByUrl -> Query all properties
39+
$XML = @"
40+
<Request AddExpandoFieldTypeSuffix="true" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="SharePoint Online PowerShell (16.0.24908.0)" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009"><Actions><ObjectPath Id="2" ObjectPathId="1" /><ObjectPath Id="4" ObjectPathId="3" /><Query Id="5" ObjectPathId="3"><Query SelectAllProperties="true"><Properties /></Query></Query></Actions><ObjectPaths><Constructor Id="1" TypeId="{268004ae-ef6b-4e9b-8425-127220d84719}" /><Method Id="3" ParentId="1" Name="GetSitePropertiesByUrl"><Parameters><Parameter Type="String">$([System.Security.SecurityElement]::Escape($SiteUrl))</Parameter><Parameter Type="Boolean">true</Parameter></Parameters></Method></ObjectPaths></Request>
41+
"@
42+
$AdditionalHeaders = @{ 'Accept' = 'application/json;odata=verbose' }
43+
$Results = New-GraphPostRequest -scope "$AdminUrl/.default" -tenantid $TenantFilter -Uri "$AdminUrl/_vti_bin/client.svc/ProcessQuery" -Type POST -Body $XML -ContentType 'text/xml' -AddedHeaders $AdditionalHeaders
44+
$Site = $Results | Where-Object { $_._ObjectType_ -match 'SiteProperties' } | Select-Object -First 1
45+
if (-not $Site) {
46+
throw "Could not retrieve site properties for $SiteUrl"
47+
}
48+
return $Site
49+
}
50+
2951
$XML = @'
3052
<Request AddExpandoFieldTypeSuffix="true" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="SharePoint Online PowerShell (16.0.24908.0)" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009"><Actions><ObjectPath Id="172" ObjectPathId="171" /><ObjectPath Id="174" ObjectPathId="173" /><Query Id="175" ObjectPathId="173"><Query SelectAllProperties="true"><Properties><Property Name="NextStartIndexFromSharePoint" ScalarProperty="true" /></Properties></Query><ChildItemQuery SelectAllProperties="true"><Properties /></ChildItemQuery></Query></Actions><ObjectPaths><Constructor Id="171" TypeId="{268004ae-ef6b-4e9b-8425-127220d84719}" /><Method Id="173" ParentId="171" Name="GetSitePropertiesFromSharePoint"><Parameters><Parameter Type="Null" /><Parameter Type="Boolean">false</Parameter></Parameters></Method></ObjectPaths></Request>
3153
'@

Modules/CIPPCore/Public/Remove-CIPPGroupMember.ps1

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,28 @@ function Remove-CIPPGroupMember {
5252
)
5353

5454
try {
55-
$Requests = foreach ($m in $Member) {
56-
if ($m -like '*#EXT#*') { $m = [System.Web.HttpUtility]::UrlEncode($m) }
55+
$Requests = @(
56+
foreach ($m in $Member) {
57+
if ($m -like '*#EXT#*') { $m = [System.Web.HttpUtility]::UrlEncode($m) }
58+
@{
59+
id = "users-$m"
60+
url = "users/$($m)?`$select=id,userPrincipalName"
61+
method = 'GET'
62+
}
63+
}
5764
@{
58-
id = $m
59-
url = "users/$($m)?`$select=id,userPrincipalName"
65+
id = 'group'
66+
url = "groups/$($GroupId)?`$select=id,displayName"
6067
method = 'GET'
6168
}
62-
}
63-
$Users = New-GraphBulkRequest -Requests @($Requests) -tenantid $TenantFilter
69+
)
70+
$BulkResults = New-GraphBulkRequest -Requests @($Requests) -tenantid $TenantFilter
71+
$Users = @($BulkResults | Where-Object { $_.id -like 'users-*' })
72+
# Group display name for logging; falls back to the id if the lookup failed
73+
# (e.g. the group was addressed by mail rather than GUID).
74+
$GroupName = ($BulkResults | Where-Object { $_.id -eq 'group' }).body.displayName ?? $GroupId
75+
$SuccessfulUsers = [System.Collections.Generic.List[string]]::new()
76+
$FailedUsers = [System.Collections.Generic.List[string]]::new()
6477

6578
if ($GroupType -eq 'Distribution list' -or $GroupType -eq 'Mail-Enabled Security') {
6679
$ExoBulkRequests = [System.Collections.Generic.List[object]]::new()
@@ -75,7 +88,7 @@ function Remove-CIPPGroupMember {
7588
}
7689
})
7790
$ExoLogs.Add(@{
78-
message = "Removed member $($User.body.userPrincipalName) from $($GroupId) group"
91+
message = "Removed member $($User.body.userPrincipalName) from group $($GroupName)"
7992
target = $User.body.userPrincipalName
8093
})
8194
}
@@ -93,6 +106,7 @@ function Remove-CIPPGroupMember {
93106
$ExoError = $LastError | Where-Object { $ExoLog.target -in $_.target -and $_.error }
94107
if (!$LastError -or ($LastError.error -and $LastError.target -notcontains $ExoLog.target)) {
95108
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $ExoLog.message -Sev 'Info'
109+
$SuccessfulUsers.Add($ExoLog.target)
96110
}
97111
}
98112
}
@@ -106,20 +120,36 @@ function Remove-CIPPGroupMember {
106120
}
107121
$RemovalResults = New-GraphBulkRequest -tenantid $TenantFilter -Requests @($RemovalRequests)
108122
foreach ($Result in $RemovalResults) {
109-
if ($Result.status -ne 204) {
110-
throw "Failed to remove member $($Result.id): $($Result.body.error.message)"
123+
$UserPrincipalName = ($Users | Where-Object { $_.body.id -eq $Result.id }).body.userPrincipalName
124+
if ($Result.status -lt 200 -or $Result.status -gt 299) {
125+
# Select-Object -First 1: Get-NormalizedError can return multiple strings
126+
# when a message matches more than one of its translation patterns.
127+
$ErrorText = Get-NormalizedError -message ($Result.body.error.message ?? "Request failed with status $($Result.status)") | Select-Object -First 1
128+
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to remove member $UserPrincipalName from group $($GroupName): $ErrorText" -Sev 'Error'
129+
$FailedUsers.Add("$UserPrincipalName ($ErrorText)")
130+
} else {
131+
$SuccessfulUsers.Add($UserPrincipalName)
111132
}
112133
}
113134
}
114-
$UserList = ($Users.body.userPrincipalName -join ', ')
115-
$Results = "Successfully removed user $UserList from $($GroupId)."
135+
$Messages = [System.Collections.Generic.List[string]]::new()
136+
if ($SuccessfulUsers.Count -gt 0) {
137+
$Messages.Add("Successfully removed user $($SuccessfulUsers -join ', ') from group $($GroupName).")
138+
}
139+
if ($FailedUsers.Count -gt 0) {
140+
$Messages.Add("Failed to remove $($FailedUsers -join '; ').")
141+
}
142+
$Results = $Messages -join ' '
143+
if ($SuccessfulUsers.Count -eq 0 -and $FailedUsers.Count -gt 0) {
144+
throw $Results
145+
}
116146
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Results -Sev Info
117147
return $Results
118148

119149
} catch {
120150
$ErrorMessage = Get-CippException -Exception $_
121151
$UserList = if ($Users) { ($Users.body.userPrincipalName -join ', ') } else { ($Member -join ', ') }
122-
$Results = "Failed to remove user $UserList from $($GroupId): $($ErrorMessage.NormalizedError)"
152+
$Results = "Failed to remove user $UserList from group $($GroupName ?? $GroupId): $($ErrorMessage.NormalizedError)"
123153
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Results -Sev Error -LogData $ErrorMessage
124154
throw $Results
125155
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
function Restore-CIPPSPODeletedSite {
2+
<#
3+
.SYNOPSIS
4+
Restore a deleted SharePoint site from the tenant recycle bin via CSOM
5+
6+
.DESCRIPTION
7+
Restores a deleted site collection using the CSOM RestoreDeletedSite method against the
8+
SharePoint admin endpoint (same ProcessQuery pattern as Set-CIPPSharePointPerms).
9+
10+
.PARAMETER TenantFilter
11+
Tenant the site belongs to
12+
13+
.PARAMETER SiteUrl
14+
Full URL of the deleted site to restore
15+
16+
.EXAMPLE
17+
Restore-CIPPSPODeletedSite -TenantFilter 'contoso.onmicrosoft.com' -SiteUrl 'https://contoso.sharepoint.com/sites/OldSite'
18+
19+
.FUNCTIONALITY
20+
Internal
21+
#>
22+
[CmdletBinding(SupportsShouldProcess = $true)]
23+
param(
24+
[Parameter(Mandatory = $true)]
25+
[string]$TenantFilter,
26+
[Parameter(Mandatory = $true)]
27+
[string]$SiteUrl
28+
)
29+
30+
$SharePointInfo = Get-SharePointAdminLink -Public $false -tenantFilter $TenantFilter
31+
$AdminUrl = $SharePointInfo.AdminUrl
32+
$AdditionalHeaders = @{ 'Accept' = 'application/json;odata=verbose' }
33+
34+
$XML = @"
35+
<Request AddExpandoFieldTypeSuffix="true" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="SharePoint Online PowerShell (16.0.24908.0)" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009"><Actions><ObjectPath Id="2" ObjectPathId="1" /><ObjectPath Id="4" ObjectPathId="3" /><Query Id="5" ObjectPathId="3"><Query SelectAllProperties="false"><Properties><Property Name="IsComplete" ScalarProperty="true" /><Property Name="PollingInterval" ScalarProperty="true" /></Properties></Query></Query></Actions><ObjectPaths><Constructor Id="1" TypeId="{268004ae-ef6b-4e9b-8425-127220d84719}" /><Method Id="3" ParentId="1" Name="RestoreDeletedSite"><Parameters><Parameter Type="String">$([System.Security.SecurityElement]::Escape($SiteUrl))</Parameter></Parameters></Method></ObjectPaths></Request>
36+
"@
37+
38+
if ($PSCmdlet.ShouldProcess($SiteUrl, 'Restore deleted site')) {
39+
$Results = New-GraphPostRequest -scope "$AdminUrl/.default" -tenantid $TenantFilter -Uri "$AdminUrl/_vti_bin/client.svc/ProcessQuery" -Type POST -Body $XML -ContentType 'text/xml' -AddedHeaders $AdditionalHeaders
40+
41+
$CsomError = ($Results | Where-Object { $_.ErrorInfo } | Select-Object -First 1).ErrorInfo.ErrorMessage
42+
if ($CsomError) { throw $CsomError }
43+
44+
return ($Results | Where-Object { $null -ne $_.IsComplete } | Select-Object -First 1)
45+
}
46+
}

0 commit comments

Comments
 (0)