Skip to content

Commit 183d38a

Browse files
fix(sharepoint): cache resolved admin portal url
Improve SharePoint admin link handling across backend and frontend. The API now requires `TenantFilter`, resolves and persists a valid admin URL, logs and returns clearer errors when resolution fails, and includes the URL in redirect responses. Tenant listing and portal UI now prefer the cached `SharepointAdminUrl` field (with fallback to the resolver endpoint), so portal links behave like other direct admin links after first resolution. Synced from CyberDrain/CIPP@81a896f
1 parent 5cbc10e commit 183d38a

3 files changed

Lines changed: 61 additions & 27 deletions

File tree

Modules/CIPPCore/Public/GraphHelper/Get-SharePointAdminLink.ps1

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,15 @@ function Get-SharePointAdminLink {
5656
throw "Failed to get SharePoint admin URL through autodiscover: $($_.Exception.Message)"
5757
}
5858
} else {
59-
$tenantName = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/sites/root' -asApp $true -tenantid $TenantFilter).id.Split('.')[0]
59+
# id looks like 'contoso.sharepoint.com,<guid>,<guid>' - the host's first label is the name.
60+
$RootSite = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/sites/root' -asApp $true -tenantid $TenantFilter
61+
$tenantName = ($RootSite.id -split '\.')[0]
62+
}
63+
64+
# Without a name every URL below is a well-formed link to nowhere ('https://-admin.sharepoint.com').
65+
# Callers cache what they get back, so a bad value here sticks around - fail instead.
66+
if ([string]::IsNullOrWhiteSpace($tenantName)) {
67+
throw "Could not determine the SharePoint tenant name for $TenantFilter. The tenant may not have SharePoint provisioned, or the Sites.Read.All permission may be missing."
6068
}
6169

6270
# Return object with all needed properties

Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointAdminUrl.ps1

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,48 +5,67 @@ function Invoke-ListSharepointAdminUrl {
55
.ROLE
66
CIPP.Core.Read
77
.DESCRIPTION
8-
Retrieves the SharePoint Admin Center URL for a tenant.
8+
Retrieves the SharePoint Admin Center URL for a tenant and redirects to it. Pass ReturnUrl to
9+
get the URL back as JSON instead of being redirected.
10+
11+
The URL cannot be derived from the tenant name - it has to be resolved through Graph - so the
12+
result is cached on the tenant, which lets ListTenants hand out a direct link from then on.
913
#>
1014
[CmdletBinding()]
1115
param(
1216
$Request,
1317
$TriggerMetadata
1418
)
1519

16-
if ($Request.Query.TenantFilter) {
17-
$TenantFilter = $Request.Query.TenantFilter
20+
$APIName = $Request.Params.CIPPEndpoint
21+
$Headers = $Request.Headers
22+
$TenantFilter = $Request.Query.TenantFilter
23+
24+
if (!$TenantFilter) {
25+
return ([HttpResponseContext]@{
26+
StatusCode = [HttpStatusCode]::BadRequest
27+
Body = @{ Results = 'TenantFilter is required' }
28+
})
29+
}
1830

19-
$Tenant = Get-Tenants -TenantFilter $TenantFilter
31+
try {
32+
$Tenant = Get-Tenants -TenantFilter $TenantFilter | Select-Object -First 1
33+
if (!$Tenant) {
34+
throw "Tenant '$TenantFilter' was not found."
35+
}
2036

2137
if ($Tenant.SharepointAdminUrl) {
2238
$AdminUrl = $Tenant.SharepointAdminUrl
2339
} else {
24-
$SharePointInfo = Get-SharePointAdminLink -Public $false -tenantFilter $TenantFilter
25-
$Tenant | Add-Member -MemberType NoteProperty -Name SharepointAdminUrl -Value $SharePointInfo.AdminUrl
40+
# Throws rather than returning a placeholder if the name can't be resolved, so we never
41+
# cache a URL that points nowhere.
42+
$AdminUrl = (Get-SharePointAdminLink -Public $false -TenantFilter $TenantFilter).AdminUrl
43+
44+
$Tenant | Add-Member -MemberType NoteProperty -Name 'SharepointAdminUrl' -Value $AdminUrl -Force
2645
$Table = Get-CIPPTable -TableName 'Tenants'
2746
Add-CIPPAzDataTableEntity @Table -Entity $Tenant -Force
28-
$AdminUrl = $SharePointInfo.AdminUrl
2947
}
48+
} catch {
49+
$ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
50+
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to resolve the SharePoint admin URL: $ErrorMessage" -Sev 'Error'
51+
return ([HttpResponseContext]@{
52+
StatusCode = [HttpStatusCode]::InternalServerError
53+
Body = @{ Results = "Could not resolve the SharePoint admin URL for $TenantFilter. $ErrorMessage" }
54+
})
55+
}
3056

31-
if ($Request.Query.ReturnUrl) {
32-
return ([HttpResponseContext]@{
33-
StatusCode = [HttpStatusCode]::OK
34-
Body = @{
35-
AdminUrl = $AdminUrl
36-
}
37-
})
38-
} else {
39-
return ([HttpResponseContext]@{
40-
StatusCode = [HttpStatusCode]::Found
41-
Headers = @{
42-
Location = $AdminUrl
43-
}
44-
})
45-
}
46-
} else {
57+
if ($Request.Query.ReturnUrl) {
4758
return ([HttpResponseContext]@{
48-
StatusCode = [HttpStatusCode]::BadRequest
49-
Body = 'TenantFilter is required'
59+
StatusCode = [HttpStatusCode]::OK
60+
Body = @{ AdminUrl = $AdminUrl }
5061
})
5162
}
63+
64+
# The body is not decoration: a browser that follows the redirect never sees it, but it means a
65+
# caller whose runtime drops the Location header gets the URL instead of a bare 'null' page.
66+
return ([HttpResponseContext]@{
67+
StatusCode = [HttpStatusCode]::Found
68+
Headers = @{ Location = $AdminUrl }
69+
Body = @{ AdminUrl = $AdminUrl }
70+
})
5271
}

Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenants.ps1

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,14 @@ function Invoke-ListTenants {
145145
@{Name = 'portal_intune'; Expression = { "https://intune.microsoft.com/$($_.defaultDomainName)" } },
146146
@{Name = 'portal_security'; Expression = { "https://security.microsoft.com/?tid=$($_.customerId)" } },
147147
@{Name = 'portal_compliance'; Expression = { "https://purview.microsoft.com/?tid=$($_.customerId)" } },
148-
@{Name = 'portal_sharepoint'; Expression = { "/api/ListSharePointAdminUrl?tenantFilter=$($_.defaultDomainName)" } },
148+
@{Name = 'portal_sharepoint'; Expression = {
149+
# Unlike the other portals, SharePoint's host name cannot be derived from the
150+
# tenant - it has to be resolved through Graph. Hand out the cached URL when we
151+
# have one so the link behaves like every other portal, and fall back to the
152+
# endpoint that resolves (and caches) it on first use.
153+
if ($_.SharepointAdminUrl) { $_.SharepointAdminUrl } else { "/api/ListSharePointAdminUrl?tenantFilter=$($_.defaultDomainName)" }
154+
}
155+
},
149156
@{Name = 'portal_platform'; Expression = { "https://admin.powerplatform.microsoft.com/account/login/$($_.customerId)" } },
150157
@{Name = 'portal_bi'; Expression = { "https://app.powerbi.com/admin-portal?ctid=$($_.customerId)" } }
151158
}

0 commit comments

Comments
 (0)