Skip to content

Commit 8e112b3

Browse files
authored
Merge pull request #547 from KelvinTegelaar/dev
[pull] dev from KelvinTegelaar:dev
2 parents 0e7f960 + 8ecaf98 commit 8e112b3

File tree

9 files changed

+379
-230
lines changed

9 files changed

+379
-230
lines changed

Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Maintenance/Push-TableCleanupTask.ps1

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ function Push-TableCleanupTask {
2525
Write-Information "Table $Table not found"
2626
}
2727
}
28+
Write-Information "#### $($Type) task complete for $($Item.TableName)"
2829
} elseif ($Type -eq 'CleanupRule') {
2930
if ($Item.Where) {
3031
$Where = [scriptblock]::Create($Item.Where)
@@ -35,6 +36,8 @@ function Push-TableCleanupTask {
3536
$DataTableProps = $Item.DataTableProps | ConvertTo-Json | ConvertFrom-Json -AsHashtable
3637
$Table = Get-CIPPTable -tablename $Item.TableName
3738
$CleanupCompleted = $false
39+
40+
$RowsRemoved = 0
3841
do {
3942
Write-Information "Fetching entities from $($Item.TableName) with filter: $($DataTableProps.Filter)"
4043
try {
@@ -43,6 +46,7 @@ function Push-TableCleanupTask {
4346
Write-Information "Removing $($Entities.Count) entities from $($Item.TableName)"
4447
try {
4548
Remove-AzDataTableEntity @Table -Entity $Entities -Force
49+
$RowsRemoved += $Entities.Count
4650
if ($DataTableProps.First -and $Entities.Count -lt $DataTableProps.First) {
4751
$CleanupCompleted = $true
4852
}
@@ -59,9 +63,10 @@ function Push-TableCleanupTask {
5963
$CleanupCompleted = $true
6064
}
6165
} while (!$CleanupCompleted)
66+
Write-Information "#### $($Type) task complete for $($Item.TableName). Rows removed: $RowsRemoved"
6267
} else {
6368
Write-Warning "Unknown task type: $Type"
6469
}
6570
}
66-
Write-Information "#### $($Type) task complete"
71+
6772
}

Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContacts.ps1

Lines changed: 40 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
using namespace System.Collections.Generic
22
using namespace System.Text.RegularExpressions
33

4-
Function Invoke-ListContacts {
4+
function Invoke-ListContacts {
55
<#
66
.FUNCTIONALITY
77
Entrypoint
@@ -18,10 +18,9 @@ Function Invoke-ListContacts {
1818
# Early validation and exit
1919
if (-not $TenantFilter) {
2020
return ([HttpResponseContext]@{
21-
StatusCode = [HttpStatusCode]::BadRequest
22-
Body = 'tenantFilter is required'
23-
})
24-
return
21+
StatusCode = [HttpStatusCode]::BadRequest
22+
Body = 'tenantFilter is required'
23+
})
2524
}
2625

2726
# Pre-compiled regex for MailTip cleaning
@@ -52,35 +51,35 @@ Function Invoke-ListContacts {
5251
$phones = if ($phoneCapacity -gt 0) {
5352
$phoneList = [List[hashtable]]::new($phoneCapacity)
5453
if ($Contact.Phone) {
55-
$phoneList.Add(@{ type = "business"; number = $Contact.Phone })
54+
$phoneList.Add(@{ type = 'business'; number = $Contact.Phone })
5655
}
5756
if ($Contact.MobilePhone) {
58-
$phoneList.Add(@{ type = "mobile"; number = $Contact.MobilePhone })
57+
$phoneList.Add(@{ type = 'mobile'; number = $Contact.MobilePhone })
5958
}
6059
$phoneList.ToArray()
6160
} else { @() }
6261

6362
return @{
64-
id = $Contact.Id
65-
displayName = $Contact.DisplayName
66-
givenName = $Contact.FirstName
67-
surname = $Contact.LastName
68-
mail = $mailAddress
69-
companyName = $Contact.Company
70-
jobTitle = $Contact.Title
71-
website = $Contact.WebPage
72-
notes = $Contact.Notes
73-
hidefromGAL = $MailContact.HiddenFromAddressListsEnabled
74-
mailTip = $cleanMailTip
63+
id = $Contact.Id
64+
displayName = $Contact.DisplayName
65+
givenName = $Contact.FirstName
66+
surname = $Contact.LastName
67+
mail = $mailAddress
68+
companyName = $Contact.Company
69+
jobTitle = $Contact.Title
70+
website = $Contact.WebPage
71+
notes = $Contact.Notes
72+
hidefromGAL = $MailContact.HiddenFromAddressListsEnabled
73+
mailTip = $cleanMailTip
7574
onPremisesSyncEnabled = $Contact.IsDirSynced
76-
addresses = @(@{
77-
street = $Contact.StreetAddress
78-
city = $Contact.City
79-
state = $Contact.StateOrProvince
80-
countryOrRegion = $Contact.CountryOrRegion
81-
postalCode = $Contact.PostalCode
82-
})
83-
phones = $phones
75+
addresses = @(@{
76+
street = $Contact.StreetAddress
77+
city = $Contact.City
78+
state = $Contact.StateOrProvince
79+
countryOrRegion = $Contact.CountryOrRegion
80+
postalCode = $Contact.PostalCode
81+
})
82+
phones = $phones
8483
}
8584
}
8685

@@ -98,20 +97,29 @@ Function Invoke-ListContacts {
9897
}
9998

10099
if (!$Contact -or !$MailContact) {
101-
throw "Contact not found or insufficient permissions"
100+
throw 'Contact not found or insufficient permissions'
102101
}
103102

104103
$ContactResponse = ConvertTo-ContactObject -Contact $Contact -MailContact $MailContact
105104

106105
} else {
107106
# Get all contacts - simplified approach
108-
Write-Host "Getting all contacts"
107+
Write-Host 'Getting all contacts'
109108

110109
$ContactResponse = New-EXORequest -tenantid $TenantFilter -cmdlet 'Get-Contact' -cmdParams @{
111-
Filter = "RecipientTypeDetails -eq 'MailContact'"
110+
Filter = "RecipientTypeDetails -eq 'MailContact'"
112111
ResultSize = 'Unlimited'
113112
} | Select-Object -Property City, Company, Department, DisplayName, FirstName, LastName, IsDirSynced, Guid, WindowsEmailAddress
114113

114+
# Add Graph ID to each contact based on email match
115+
$GraphContacts = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/contacts' -tenantid $TenantFilter
116+
foreach ($contact in $ContactResponse) {
117+
$GraphMatch = $GraphContacts | Where-Object { $_.mail -eq $contact.WindowsEmailAddress }
118+
if ($GraphMatch) {
119+
$contact | Add-Member -MemberType NoteProperty -Name 'graphId' -Value $GraphMatch.id -Force
120+
}
121+
}
122+
115123
# Return empty array if no contacts found
116124
if (!$ContactResponse) {
117125
$ContactResponse = @()
@@ -128,7 +136,7 @@ Function Invoke-ListContacts {
128136
}
129137

130138
return ([HttpResponseContext]@{
131-
StatusCode = $StatusCode
132-
Body = $ContactResponse
133-
})
139+
StatusCode = $StatusCode
140+
Body = $ContactResponse
141+
})
134142
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
function Invoke-ExecSetCloudManaged {
2+
<#
3+
.FUNCTIONALITY
4+
Entrypoint
5+
.ROLE
6+
Identity.DirSync.ReadWrite
7+
.DESCRIPTION
8+
Sets the cloud-managed status of a user, group, or contact.
9+
#>
10+
[CmdletBinding()]
11+
param($Request, $TriggerMetadata)
12+
13+
$APIName = $Request.Params.CIPPEndpoint
14+
$Headers = $Request.Headers
15+
16+
# Interact with query parameters or the body of the request.
17+
$TenantFilter = $Request.Body.tenantFilter
18+
$GroupID = $Request.Body.ID
19+
$DisplayName = $Request.Body.displayName
20+
$Type = $Request.Body.type
21+
$IsCloudManaged = [System.Convert]::ToBoolean($Request.Body.isCloudManaged)
22+
23+
try {
24+
$Params = @{
25+
Id = $GroupID
26+
TenantFilter = $TenantFilter
27+
DisplayName = $DisplayName
28+
Type = $Type
29+
IsCloudManaged = $IsCloudManaged
30+
APIName = $APIName
31+
Headers = $Headers
32+
}
33+
$Result = Set-CIPPCloudManaged @Params
34+
$StatusCode = [HttpStatusCode]::OK
35+
} catch {
36+
$Result = "$($_.Exception.Message)"
37+
$StatusCode = [HttpStatusCode]::InternalServerError
38+
}
39+
return ([HttpResponseContext]@{
40+
StatusCode = $StatusCode
41+
Body = @{'Results' = $Result }
42+
})
43+
}

Modules/CIPPCore/Public/Remove-CIPPGroups.ps1

Lines changed: 113 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,43 +11,127 @@ function Remove-CIPPGroups {
1111
if (-not $userid) {
1212
$UserID = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$($Username)" -tenantid $TenantFilter).id
1313
}
14-
$AllGroups = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/groups/?`$select=displayName,mailEnabled,id,groupTypes,assignedLicenses&`$top=999" -tenantid $TenantFilter)
14+
$AllGroups = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/groups/?`$select=displayName,mailEnabled,id,groupTypes,assignedLicenses,onPremisesSyncEnabled,membershipRule&`$top=999" -tenantid $TenantFilter)
1515

16-
$Returnval = (New-GraphPostRequest -uri "https://graph.microsoft.com/beta/users/$($UserID)/GetMemberGroups" -tenantid $TenantFilter -type POST -body '{"securityEnabledOnly": false}').value | ForEach-Object -Parallel {
17-
Import-Module '.\Modules\AzBobbyTables'
18-
Import-Module '.\Modules\CIPPCore'
19-
$Group = $_
16+
# Get user's groups
17+
$UserGroups = (New-GraphPostRequest -uri "https://graph.microsoft.com/beta/users/$($UserID)/GetMemberGroups" -tenantid $TenantFilter -type POST -body '{"securityEnabledOnly": false}').value
2018

21-
try {
22-
$GroupName = ($using:AllGroups | Where-Object -Property id -EQ $Group).displayName
23-
$IsMailEnabled = ($using:AllGroups | Where-Object -Property id -EQ $Group).mailEnabled
24-
$IsM365Group = $null -ne ($using:AllGroups | Where-Object { $_.id -eq $Group -and $_.groupTypes -contains 'Unified' })
25-
$IsLicensed = ($using:AllGroups | Where-Object -Property id -EQ $Group).assignedLicenses.Count -gt 0
26-
27-
if ($IsLicensed) {
28-
"Could not remove $($using:Username) from $GroupName. This is because the group has licenses assigned to it."
29-
} else {
30-
if ($IsM365Group) {
31-
$null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/groups/$_/members/$($using:UserID)/`$ref" -tenantid $using:TenantFilter -type DELETE -body '' -Verbose
32-
} elseif (-not $IsMailEnabled) {
33-
$null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/groups/$_/members/$($using:UserID)/`$ref" -tenantid $using:TenantFilter -type DELETE -body '' -Verbose
34-
} elseif ($IsMailEnabled) {
35-
$Params = @{ Identity = $GroupName; Member = $using:UserID ; BypassSecurityGroupManagerCheck = $true }
36-
New-ExoRequest -tenantid $using:tenantFilter -cmdlet 'Remove-DistributionGroupMember' -cmdParams $params -UseSystemMailbox $true
19+
if (-not $UserGroups) {
20+
$Returnval = "$($Username) is not a member of any groups."
21+
Write-LogMessage -headers $Headers -API $APIName -message "$($Username) is not a member of any groups" -Sev 'Info' -tenant $TenantFilter
22+
return $Returnval
23+
}
24+
25+
# Initialize bulk request arrays and results
26+
$BulkRequests = [System.Collections.Generic.List[object]]::new()
27+
$ExoBulkRequests = [System.Collections.Generic.List[object]]::new()
28+
$GraphLogs = [System.Collections.Generic.List[object]]::new()
29+
$ExoLogs = [System.Collections.Generic.List[object]]::new()
30+
$Results = [System.Collections.Generic.List[string]]::new()
31+
32+
# Process each group and prepare bulk requests
33+
foreach ($Group in $UserGroups) {
34+
$GroupInfo = $AllGroups | Where-Object -Property id -EQ $Group
35+
$GroupName = $GroupInfo.displayName
36+
$IsMailEnabled = $GroupInfo.mailEnabled
37+
$IsM365Group = $null -ne ($AllGroups | Where-Object { $_.id -eq $Group -and $_.groupTypes -contains 'Unified' })
38+
$IsLicensed = $GroupInfo.assignedLicenses.Count -gt 0
39+
$IsDynamic = -not [string]::IsNullOrWhiteSpace($GroupInfo.membershipRule)
40+
41+
if ($IsLicensed) {
42+
$Results.Add("Could not remove $Username from group '$GroupName' because it has assigned licenses. These groups are removed during the license removal step.")
43+
Write-LogMessage -headers $Headers -API $APIName -message "Could not remove $Username from group '$GroupName' because it has assigned licenses. These groups are removed during the license removal step." -Sev 'Warning' -tenant $TenantFilter
44+
} elseif ($IsDynamic) {
45+
$Results.Add("Error: Could not remove $Username from group '$GroupName' because it is a Dynamic Group.")
46+
Write-LogMessage -headers $Headers -API $APIName -message "Could not remove $Username from group '$GroupName' because it is a Dynamic Group." -Sev 'Warning' -tenant $TenantFilter
47+
} elseif ($GroupInfo.onPremisesSyncEnabled) {
48+
$Results.Add("Error: Could not remove $Username from group '$GroupName' because it is synced with Active Directory.")
49+
Write-LogMessage -headers $Headers -API $APIName -message "Could not remove $Username from group '$GroupName' because it is synced with Active Directory." -Sev 'Warning' -tenant $TenantFilter
50+
} else {
51+
if ($IsM365Group -or (-not $IsMailEnabled)) {
52+
# Use Graph API for M365 Groups and Security Groups
53+
$BulkRequests.Add(@{
54+
id = "removeFromGroup-$Group"
55+
method = 'DELETE'
56+
url = "groups/$Group/members/$UserID/`$ref"
57+
})
58+
$GraphLogs.Add(@{
59+
message = "Removed $Username from $GroupName"
60+
id = "removeFromGroup-$Group"
61+
groupName = $GroupName
62+
})
63+
} elseif ($IsMailEnabled) {
64+
# Use Exchange Online for Distribution Lists
65+
$Params = @{
66+
Identity = $GroupName
67+
Member = $UserID
68+
BypassSecurityGroupManagerCheck = $true
3769
}
70+
$ExoBulkRequests.Add(@{
71+
CmdletInput = @{
72+
CmdletName = 'Remove-DistributionGroupMember'
73+
Parameters = $Params
74+
}
75+
})
76+
$ExoLogs.Add(@{
77+
message = "Removed $Username from $GroupName"
78+
target = $UserID
79+
groupName = $GroupName
80+
})
81+
}
82+
}
83+
}
3884

39-
Write-LogMessage -headers $using:Headers -API $($using:APIName) -message "Removed $($using:Username) from $GroupName" -Sev 'Info' -tenant $using:TenantFilter
40-
"Successfully removed $($using:Username) from group $GroupName"
85+
# Execute Graph bulk requests
86+
if ($BulkRequests.Count -gt 0) {
87+
try {
88+
$RawGraphRequest = New-GraphBulkRequest -tenantid $TenantFilter -scope 'https://graph.microsoft.com/.default' -Requests @($BulkRequests) -asapp $true
89+
90+
foreach ($GraphLog in $GraphLogs) {
91+
$GraphError = $RawGraphRequest | Where-Object { $_.id -eq $GraphLog.id -and $_.status -notmatch '^2[0-9]+' }
92+
if ($GraphError) {
93+
$Message = Get-NormalizedError -message $GraphError.body.error
94+
$Results.Add("Could not remove $Username from group '$($GraphLog.groupName)': $Message. This is likely because it's a Dynamic Group or synced with Active Directory")
95+
Write-LogMessage -headers $Headers -API $APIName -message "Could not remove $Username from group '$($GraphLog.groupName)': $Message" -Sev 'Error' -tenant $TenantFilter
96+
} else {
97+
$Results.Add("Successfully removed $Username from group '$($GraphLog.groupName)'")
98+
Write-LogMessage -headers $Headers -API $APIName -message $GraphLog.message -Sev 'Info' -tenant $TenantFilter
99+
}
41100
}
42101
} catch {
43102
$ErrorMessage = Get-CippException -Exception $_
44-
Write-LogMessage -headers $using:Headers -API $($using:APIName) -message "Could not remove $($using:Username) from group $GroupName : $($ErrorMessage.NormalizedError)" -Sev 'Error' -tenant $using:TenantFilter -LogData $ErrorMessage
45-
"Could not remove $($using:Username) from group $($GroupName): $($ErrorMessage.NormalizedError). This is likely because its a Dynamic Group or synched with active directory"
103+
Write-LogMessage -headers $Headers -API $APIName -message "Error executing Graph bulk requests: $($ErrorMessage.NormalizedError)" -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage
104+
$Results.Add("Error executing bulk removal requests: $($ErrorMessage.NormalizedError)")
46105
}
47106
}
48-
if (-not $Returnval) {
49-
$Returnval = "$($Username) is not a member of any groups."
50-
Write-LogMessage -headers $Headers -API $APIName -message "$($Username) is not a member of any groups" -Sev 'Info' -tenant $TenantFilter
107+
108+
# Execute Exchange Online bulk requests
109+
if ($ExoBulkRequests.Count -gt 0) {
110+
try {
111+
$RawExoRequest = New-ExoBulkRequest -tenantid $TenantFilter -cmdletArray @($ExoBulkRequests)
112+
$LastError = $RawExoRequest | Select-Object -Last 1
113+
114+
foreach ($ExoError in $LastError.error) {
115+
$Results.Add("Error - $ExoError")
116+
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $ExoError -Sev 'Error'
117+
}
118+
119+
foreach ($ExoLog in $ExoLogs) {
120+
$ExoError = $LastError | Where-Object { $ExoLog.target -in $_.target -and $_.error }
121+
if (!$LastError -or ($LastError.error -and $LastError.target -notcontains $ExoLog.target)) {
122+
$Results.Add("Successfully removed $Username from group $($ExoLog.groupName)")
123+
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $ExoLog.message -Sev 'Info'
124+
} else {
125+
$Results.Add("Could not remove $Username from $($ExoLog.groupName). This is likely because its a Dynamic Group or synched with active directory")
126+
Write-LogMessage -headers $Headers -API $APIName -message "Could not remove $Username from $($ExoLog.groupName)" -Sev 'Error' -tenant $TenantFilter
127+
}
128+
}
129+
} catch {
130+
$ErrorMessage = Get-CippException -Exception $_
131+
Write-LogMessage -headers $Headers -API $APIName -message "Error executing Exchange bulk requests: $($ErrorMessage.NormalizedError)" -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage
132+
$Results.Add("Error executing bulk Exchange requests: $($ErrorMessage.NormalizedError)")
133+
}
51134
}
52-
return $Returnval
135+
136+
return $Results
53137
}

Modules/CIPPCore/Public/SAMManifest.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,18 @@
538538
{
539539
"id": "0a42382f-155c-4eb1-9bdc-21548ccaa387",
540540
"type": "Role"
541+
},
542+
{
543+
"id": "2d9bd318-b883-40be-9df7-63ec4fcdc424",
544+
"type": "Role"
545+
},
546+
{
547+
"id": "c8948c23-e66b-42db-83fd-770b71ab78d2",
548+
"type": "Role"
549+
},
550+
{
551+
"id": "a94a502d-0281-4d15-8cd2-682ac9362c4c",
552+
"type": "Role"
541553
}
542554
]
543555
},

0 commit comments

Comments
 (0)