Skip to content

Commit 396606f

Browse files
committed
Implement rest api management of teams admin management and update cache, tests and standards
This addressed authentication issues with GDAP and the Teams PowerShell module, not all uses of the module have been replaced but this covers most of them with the remaining to be ported at a later date allowing for the complete removal of the teams PowerShell module
1 parent 7c1afbf commit 396606f

24 files changed

Lines changed: 344 additions & 110 deletions
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
function New-TeamsRequestV2 {
2+
<#
3+
.SYNOPSIS
4+
Talks to the Teams admin ConfigAPI (api.interfaces.records.teams.microsoft.com)
5+
directly, instead of the MicrosoftTeams PowerShell module.
6+
7+
.DESCRIPTION
8+
The MicrosoftTeams module routes policy-definition writes through the legacy
9+
/Skype.Policy/tenants/policies surface, which returns 40301 Forbidden under CSP/GDAP
10+
(for both delegate-with-Teams-Admin and app-only-with-Global-Admin tokens). The Teams
11+
admin center (ACMS) instead uses /Skype.Policy/configurations/{Type}/configuration/{Identity},
12+
which authorizes fine with the roles CIPP already has. This helper speaks that surface.
13+
14+
All HTTP goes through Invoke-CIPPRestMethod (pooled CIPP.CIPPRestClient) so gzip
15+
responses are decompressed and JSON is deserialized consistently across platforms
16+
(raw Invoke-RestMethod does NOT decompress gzip in the Linux worker, which yields
17+
garbage/null objects and unreadable error bodies).
18+
19+
Operations:
20+
Get -> GET configurations/{Type}/configuration/{Identity} (single object)
21+
Get -ListAll -> GET configurations/{Type} (all instances, array)
22+
Set -> PUT configurations/{Type}/configuration/{Identity} (MERGE; only changed props)
23+
New -> PUT configurations/{Type}/configuration/{Identity} (create named policy) [best-effort]
24+
Remove -> DELETE configurations/{Type}/configuration/{Identity} [best-effort]
25+
26+
.PARAMETER TenantFilter
27+
Target tenant (GUID or default domain).
28+
29+
.PARAMETER Type
30+
ConfigAPI type (e.g. 'TeamsMeetingPolicy'), a cmdlet noun, or a full cmdlet name
31+
(e.g. 'Set-CsTenantFederationConfiguration'); the verb is stripped and known
32+
noun->type aliases are applied automatically.
33+
34+
.PARAMETER Action
35+
Get (default) | Set | New | Remove.
36+
37+
.PARAMETER Identity
38+
Policy instance identity. Default 'Global'. (e.g. 'Tag:Default' or a custom name.)
39+
40+
.PARAMETER Parameters
41+
Hashtable of properties to write (Set/New). Only these are sent (merge).
42+
43+
.PARAMETER ListAll
44+
Get: return every instance of the type (array) instead of one identity.
45+
46+
.PARAMETER AsApp
47+
Use an app-only (client_credentials) token instead of the delegate token.
48+
49+
.PARAMETER NoRead
50+
Set: skip the read-for-Key step and PUT only the bare changed props (ACMS-style).
51+
Faster; safe for flat/Host-authority types and federation. Default is read-modify-write
52+
(adds the Key envelope when the type carries one) for maximum compatibility.
53+
54+
.PARAMETER UseServiceDiscovery
55+
Resolve the per-tenant ConfigApi host + X-MS-Forest via Teams.Tenant/tenants and use
56+
them (and, for federation/ACS types, the OcsPowershellWebservice target headers).
57+
58+
.EXAMPLE
59+
New-TeamsRequestV2 -TenantFilter $t -Type TeamsMeetingPolicy -Action Set -Parameters @{ AllowAnonymousUsersToJoinMeeting = $false }
60+
61+
.FUNCTIONALITY
62+
Internal
63+
#>
64+
[CmdletBinding()]
65+
param(
66+
[Parameter(Mandatory)] $TenantFilter,
67+
[Parameter(Mandatory)] [string] $Type,
68+
[ValidateSet('Get', 'Set', 'New', 'Remove')] [string] $Action = 'Get',
69+
[string] $Identity = 'Global',
70+
[hashtable] $Parameters = @{},
71+
[switch] $ListAll,
72+
[switch] $AsApp,
73+
[switch] $NoRead,
74+
[switch] $UseServiceDiscovery
75+
)
76+
77+
# ---- cmdlet-noun -> ConfigAPI type aliases (noun != type) ----
78+
$TypeAliases = @{
79+
'TenantFederationConfiguration' = 'TenantFederationSettings'
80+
}
81+
# types backed by the legacy OcsPowershellWebservice (need target-uri headers via discovery)
82+
$FederationTypes = @('TenantFederationSettings', 'TeamsAcsFederationConfiguration')
83+
84+
# normalize Type: strip Get-/Set-/New-/Remove-Cs prefix, then alias
85+
$ConfigType = $Type -replace '^(Get|Set|New|Remove|Grant|Revoke)-Cs', ''
86+
if ($TypeAliases.ContainsKey($ConfigType)) { $ConfigType = $TypeAliases[$ConfigType] }
87+
88+
# ---- token ----
89+
$TokenSplat = @{ tenantid = $TenantFilter; scope = '48ac35b8-9aa8-4d74-927d-1f4a14a0b239/.default' }
90+
if ($AsApp) { $TokenSplat['AsApp'] = $true }
91+
$TeamsToken = (Get-GraphToken @TokenSplat).Authorization -replace 'Bearer '
92+
93+
# ---- endpoint + forest (service discovery optional) ----
94+
$ApiHost = 'api.interfaces.records.teams.microsoft.com'
95+
$Forest = $null
96+
$AdminServiceEndpoint = $null
97+
$AdminDomain = $null
98+
if ($UseServiceDiscovery) {
99+
try {
100+
$disc = Invoke-CIPPRestMethod -Uri "https://$ApiHost/Teams.Tenant/tenants" -Method GET `
101+
-Headers @{ Authorization = "Bearer $TeamsToken"; Accept = 'application/json' }
102+
if ($disc.serviceDiscovery.Endpoints.ConfigApiEndpoint) { $ApiHost = $disc.serviceDiscovery.Endpoints.ConfigApiEndpoint }
103+
$Forest = $disc.serviceDiscovery.Headers.'X-MS-Forest'
104+
$AdminServiceEndpoint = $disc.serviceDiscovery.Endpoints.AdminServiceEndpoint
105+
$AdminDomain = ($disc.verifiedDomains | Where-Object { $_.name -like '*.onmicrosoft.com' -and $_.name -notlike '*.*.onmicrosoft.com' } | Select-Object -First 1).name
106+
} catch {
107+
Write-Verbose "Service discovery failed, using defaults: $($_.Exception.Message)"
108+
}
109+
}
110+
$Base = "https://$ApiHost/Skype.Policy/configurations/$ConfigType"
111+
112+
# ---- headers ----
113+
$Headers = @{
114+
Authorization = "Bearer $TeamsToken"
115+
Accept = 'application/json'
116+
'x-authz-scope' = 'tenant'
117+
'x-ms-correlation-id' = (New-Guid).Guid
118+
}
119+
if ($Forest) { $Headers['x-ms-forest'] = $Forest }
120+
if ($ConfigType -in $FederationTypes -and $AdminServiceEndpoint) {
121+
$Headers['x-ms-target-uri'] = "https://$AdminServiceEndpoint/OcsPowershellWebservice"
122+
$Headers['x-ms-tenant-id'] = $TenantFilter
123+
}
124+
$Query = if ($ConfigType -in $FederationTypes -and $AdminDomain) { "?adminDomain=$AdminDomain" } else { '' }
125+
126+
switch ($Action) {
127+
128+
'Get' {
129+
$Uri = if ($ListAll) { "$Base$Query" } else { "$Base/configuration/$Identity$Query" }
130+
return Invoke-CIPPRestMethod -Uri $Uri -Method GET -Headers $Headers
131+
}
132+
133+
'Set' {
134+
$Uri = "$Base/configuration/$Identity$Query"
135+
136+
# build body of only the changed props (skip control keys)
137+
$Body = [ordered]@{}
138+
foreach ($k in $Parameters.Keys) {
139+
if ($k -in @('Identity', 'ErrorAction', 'Confirm', 'WhatIf', 'Verbose', 'Debug')) { continue }
140+
$Body[$k] = $Parameters[$k]
141+
}
142+
143+
if (-not $NoRead) {
144+
# read-modify-write: include the Key envelope when the type carries one (max compat)
145+
try {
146+
$Current = Invoke-CIPPRestMethod -Uri $Uri -Method GET -Headers $Headers
147+
if ($Current -and ($Current.PSObject.Properties.Name -contains 'Key')) {
148+
$Merged = [ordered]@{ Identity = $Identity; Key = $Current.Key }
149+
foreach ($k in $Body.Keys) { $Merged[$k] = $Body[$k] }
150+
$Body = $Merged
151+
}
152+
} catch {
153+
Write-Verbose "Pre-read failed ($($_.Exception.Message)); sending bare props."
154+
}
155+
}
156+
157+
$Json = $Body | ConvertTo-Json -Depth 25 -Compress
158+
$StatusCode = $null
159+
$RespBody = Invoke-CIPPRestMethod -Uri $Uri -Method PUT -Body $Json -ContentType 'application/json' `
160+
-Headers $Headers -SkipHttpErrorCheck -StatusCodeVariable StatusCode
161+
if ([int]$StatusCode -ge 400) {
162+
$Detail = if ($RespBody -is [string]) { $RespBody } elseif ($null -ne $RespBody) { $RespBody | ConvertTo-Json -Compress -Depth 10 } else { '' }
163+
throw "Teams ConfigApi Set $ConfigType/$Identity failed: $StatusCode $Detail"
164+
}
165+
return [pscustomobject]@{ Type = $ConfigType; Identity = $Identity; StatusCode = [int]$StatusCode }
166+
}
167+
168+
'New' {
169+
# best-effort: create a named policy by PUTting the props at a new identity
170+
$Uri = "$Base/configuration/$Identity$Query"
171+
$Body = [ordered]@{ Identity = $Identity }
172+
foreach ($k in $Parameters.Keys) {
173+
if ($k -in @('Identity', 'ErrorAction', 'Confirm', 'WhatIf', 'Verbose', 'Debug')) { continue }
174+
$Body[$k] = $Parameters[$k]
175+
}
176+
$Json = $Body | ConvertTo-Json -Depth 25 -Compress
177+
$StatusCode = $null
178+
$RespBody = Invoke-CIPPRestMethod -Uri $Uri -Method PUT -Body $Json -ContentType 'application/json' `
179+
-Headers $Headers -SkipHttpErrorCheck -StatusCodeVariable StatusCode
180+
if ([int]$StatusCode -ge 400) {
181+
$Detail = if ($RespBody -is [string]) { $RespBody } elseif ($null -ne $RespBody) { $RespBody | ConvertTo-Json -Compress -Depth 10 } else { '' }
182+
throw "Teams ConfigApi New $ConfigType/$Identity failed: $StatusCode $Detail"
183+
}
184+
return [pscustomobject]@{ Type = $ConfigType; Identity = $Identity; StatusCode = [int]$StatusCode }
185+
}
186+
187+
'Remove' {
188+
$Uri = "$Base/configuration/$Identity$Query"
189+
$StatusCode = $null
190+
$RespBody = Invoke-CIPPRestMethod -Uri $Uri -Method DELETE -Headers $Headers -SkipHttpErrorCheck -StatusCodeVariable StatusCode
191+
if ([int]$StatusCode -ge 400) {
192+
$Detail = if ($RespBody -is [string]) { $RespBody } elseif ($null -ne $RespBody) { $RespBody | ConvertTo-Json -Compress -Depth 10 } else { '' }
193+
throw "Teams ConfigApi Remove $ConfigType/$Identity failed: $StatusCode $Detail"
194+
}
195+
return [pscustomobject]@{ Type = $ConfigType; Identity = $Identity; StatusCode = [int]$StatusCode }
196+
}
197+
}
198+
}

Modules/CIPPCore/Public/Invoke-CIPPDBCacheCollection.ps1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ function Invoke-CIPPDBCacheCollection {
150150
'CsExternalAccessPolicy'
151151
'CsTenantFederationConfiguration'
152152
'CsTeamsMessagingPolicy'
153+
'CsTeamsMessagingConfiguration'
153154
'CsTeamsAppPermissionPolicy'
154155
'Teams'
155156
'TeamsActivity'

Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCsExternalAccessPolicy.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ function Set-CIPPDBCacheCsExternalAccessPolicy {
2424
try {
2525
Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Teams External Access Policy' -sev Debug
2626

27-
$ExternalAccess = New-TeamsRequest -TenantFilter $TenantFilter -Cmdlet 'Get-CsExternalAccessPolicy' -CmdParams @{ Identity = 'Global' }
27+
$ExternalAccess = New-TeamsRequestV2 -TenantFilter $TenantFilter -Type 'ExternalAccessPolicy' -Action Get -Identity 'Global'
2828

2929
if ($ExternalAccess) {
3030
$Data = @($ExternalAccess)

Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCsTeamsAppPermissionPolicy.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ function Set-CIPPDBCacheCsTeamsAppPermissionPolicy {
2424
try {
2525
Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Teams App Permission Policies' -sev Debug
2626

27-
$AppPermissionPolicies = New-TeamsRequest -TenantFilter $TenantFilter -Cmdlet 'Get-CsTeamsAppPermissionPolicy'
27+
$AppPermissionPolicies = New-TeamsRequestV2 -TenantFilter $TenantFilter -Type 'TeamsAppPermissionPolicy' -Action Get -ListAll
2828

2929
if ($AppPermissionPolicies) {
3030
$Data = @($AppPermissionPolicies)

Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCsTeamsClientConfiguration.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ function Set-CIPPDBCacheCsTeamsClientConfiguration {
2525
try {
2626
Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Teams Client Configuration' -sev Debug
2727

28-
$ClientConfig = New-TeamsRequest -TenantFilter $TenantFilter -Cmdlet 'Get-CsTeamsClientConfiguration' -CmdParams @{ Identity = 'Global' }
28+
$ClientConfig = New-TeamsRequestV2 -TenantFilter $TenantFilter -Type 'TeamsClientConfiguration' -Action Get -Identity 'Global'
2929

3030
if ($ClientConfig) {
3131
$Data = @($ClientConfig)

Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCsTeamsMeetingPolicy.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ function Set-CIPPDBCacheCsTeamsMeetingPolicy {
2424
try {
2525
Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Teams Meeting Policy' -sev Debug
2626

27-
$MeetingPolicy = New-TeamsRequest -TenantFilter $TenantFilter -Cmdlet 'Get-CsTeamsMeetingPolicy' -CmdParams @{ Identity = 'Global' }
27+
$MeetingPolicy = New-TeamsRequestV2 -TenantFilter $TenantFilter -Type 'TeamsMeetingPolicy' -Action Get -Identity 'Global'
2828

2929
if ($MeetingPolicy) {
3030
$Data = @($MeetingPolicy)
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
function Set-CIPPDBCacheCsTeamsMessagingConfiguration {
2+
<#
3+
.SYNOPSIS
4+
Caches the Teams Messaging Configuration (Global)
5+
6+
.DESCRIPTION
7+
Calls the Teams ConfigAPI (TeamsMessagingConfiguration) via New-TeamsRequestV2 and
8+
writes the result into the CippReportingDB under Type 'CsTeamsMessagingConfiguration'.
9+
Holds the org-wide message-safety settings (FileTypeCheck, UrlReputationCheck,
10+
ContentBasedPhishingCheck, ReportIncorrectSecurityDetections, etc.).
11+
12+
.PARAMETER TenantFilter
13+
The tenant to cache the messaging configuration for
14+
15+
.PARAMETER QueueId
16+
The queue ID to update with total tasks (optional)
17+
#>
18+
[CmdletBinding()]
19+
param(
20+
[Parameter(Mandatory = $true)]
21+
[string]$TenantFilter,
22+
[string]$QueueId
23+
)
24+
25+
try {
26+
Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Teams Messaging Configuration' -sev Debug
27+
28+
$MessagingConfig = New-TeamsRequestV2 -TenantFilter $TenantFilter -Type 'TeamsMessagingConfiguration' -Action Get -Identity 'Global'
29+
30+
if ($MessagingConfig) {
31+
$Data = @($MessagingConfig)
32+
Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CsTeamsMessagingConfiguration' -Data $Data -AddCount
33+
Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached Teams Messaging Configuration' -sev Debug
34+
}
35+
$MessagingConfig = $null
36+
37+
} catch {
38+
Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache Teams Messaging Configuration: $($_.Exception.Message)" -sev Error
39+
}
40+
}

Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCsTeamsMessagingPolicy.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ function Set-CIPPDBCacheCsTeamsMessagingPolicy {
2525
try {
2626
Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Teams Messaging Policy' -sev Debug
2727

28-
$MessagingPolicy = New-TeamsRequest -TenantFilter $TenantFilter -Cmdlet 'Get-CsTeamsMessagingPolicy' -CmdParams @{ Identity = 'Global' }
28+
$MessagingPolicy = New-TeamsRequestV2 -TenantFilter $TenantFilter -Type 'TeamsMessagingPolicy' -Action Get -Identity 'Global'
2929

3030
if ($MessagingPolicy) {
3131
$Data = @($MessagingPolicy)

Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheCsTenantFederationConfiguration.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ function Set-CIPPDBCacheCsTenantFederationConfiguration {
2525
try {
2626
Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Teams Tenant Federation Configuration' -sev Debug
2727

28-
$Federation = New-TeamsRequest -TenantFilter $TenantFilter -Cmdlet 'Get-CsTenantFederationConfiguration' -CmdParams @{ Identity = 'Global' }
28+
$Federation = New-TeamsRequestV2 -TenantFilter $TenantFilter -Type 'TenantFederationConfiguration' -Action Get -Identity 'Global'
2929

3030
if ($Federation) {
3131
$Data = @($Federation)

Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardTeamsChatProtection.ps1

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ function Invoke-CIPPStandardTeamsChatProtection {
4646
} #we're done.
4747

4848
try {
49-
$CurrentState = New-TeamsRequest -TenantFilter $Tenant -Cmdlet 'Get-CsTeamsMessagingConfiguration' | Select-Object -Property Identity, FileTypeCheck, UrlReputationCheck
49+
$CurrentState = New-TeamsRequestV2 -TenantFilter $Tenant -Type 'TeamsMessagingConfiguration' -Action Get -Identity 'Global' | Select-Object -Property Identity, FileTypeCheck, UrlReputationCheck
5050
} catch {
5151
$ErrorMessage = Get-CippException -Exception $_
5252
Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the Teams Chat Protection state for $Tenant. Error: $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage
@@ -75,7 +75,7 @@ function Invoke-CIPPStandardTeamsChatProtection {
7575
}
7676

7777
try {
78-
$null = New-TeamsRequest -TenantFilter $Tenant -Cmdlet 'Set-CsTeamsMessagingConfiguration' -CmdParams $cmdParams
78+
$null = New-TeamsRequestV2 -TenantFilter $Tenant -Type 'TeamsMessagingConfiguration' -Action Set -Parameters $cmdParams
7979
Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully updated Teams Chat Protection settings to FileTypeCheck: $FileTypeCheckState, UrlReputationCheck: $UrlReputationCheckState" -sev Info
8080
} catch {
8181
$ErrorMessage = Get-CippException -Exception $_

0 commit comments

Comments
 (0)