From fcc4a39f3ed4dc1c990b633bcf31c23c0f5f6736 Mon Sep 17 00:00:00 2001 From: Gautam Sheth Date: Sun, 31 May 2026 17:38:28 +0300 Subject: [PATCH 1/2] Add Get-PnPMultiGeoCompanyAllowedDataLocation cmdlet and related model for allowed data locations --- ...t-PnPMultiGeoCompanyAllowedDataLocation.md | 57 +++++++++ .../GetMultiGeoCompanyAllowedDataLocation.cs | 21 ++++ .../MultiGeoCompanyAllowedDataLocation.cs | 23 ++++ .../MultiGeo/MultiGeoRestApiClient.cs | 117 +++++++++++++++++- 4 files changed, 215 insertions(+), 3 deletions(-) create mode 100644 documentation/Get-PnPMultiGeoCompanyAllowedDataLocation.md create mode 100644 src/Commands/Admin/GetMultiGeoCompanyAllowedDataLocation.cs create mode 100644 src/Commands/Model/MultiGeoCompanyAllowedDataLocation.cs diff --git a/documentation/Get-PnPMultiGeoCompanyAllowedDataLocation.md b/documentation/Get-PnPMultiGeoCompanyAllowedDataLocation.md new file mode 100644 index 000000000..05192afad --- /dev/null +++ b/documentation/Get-PnPMultiGeoCompanyAllowedDataLocation.md @@ -0,0 +1,57 @@ +--- +Module Name: PnP.PowerShell +title: Get-PnPMultiGeoCompanyAllowedDataLocation +schema: 2.0.0 +applicable: SharePoint Online +external help file: PnP.PowerShell.dll-Help.xml +online version: https://pnp.github.io/powershell/cmdlets/Get-PnPMultiGeoCompanyAllowedDataLocation.html +--- + +# Get-PnPMultiGeoCompanyAllowedDataLocation + +## SYNOPSIS +Returns the multi-geo data locations allowed for the SharePoint Online tenant. + +## SYNTAX + +```powershell +Get-PnPMultiGeoCompanyAllowedDataLocation [-Connection ] +``` + +## DESCRIPTION +Returns the SharePoint Online multi-geo data locations configured for the tenant, including each location code, the associated domain, and whether the location is the default location. + +## EXAMPLES + +### EXAMPLE 1 + +```powershell +Get-PnPMultiGeoCompanyAllowedDataLocation +``` + +Returns all allowed multi-geo data locations for the current tenant. + +## PARAMETERS + +### -Connection +Optional connection to be used by the cmdlet. Retrieve the value for this parameter by specifying `-ReturnConnection` on `Connect-PnPOnline` or by executing `Get-PnPConnection`. + +```yaml +Type: PnPConnection +Parameter Sets: (All) + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +## OUTPUTS + +### PnP.PowerShell.Commands.Model.MultiGeoCompanyAllowedDataLocation +Returns objects with `Location`, `Domain`, and `IsDefault` properties. + +## RELATED LINKS + +[Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) \ No newline at end of file diff --git a/src/Commands/Admin/GetMultiGeoCompanyAllowedDataLocation.cs b/src/Commands/Admin/GetMultiGeoCompanyAllowedDataLocation.cs new file mode 100644 index 000000000..6569c9f67 --- /dev/null +++ b/src/Commands/Admin/GetMultiGeoCompanyAllowedDataLocation.cs @@ -0,0 +1,21 @@ +using PnP.PowerShell.Commands.Attributes; +using PnP.PowerShell.Commands.Base; +using PnP.PowerShell.Commands.Model; +using PnP.PowerShell.Commands.Utilities.MultiGeo; +using System.Management.Automation; + +namespace PnP.PowerShell.Commands.Admin +{ + [Cmdlet(VerbsCommon.Get, "PnPMultiGeoCompanyAllowedDataLocation")] + [RequiredApiApplicationPermissions("sharepoint/Sites.FullControl.All")] + [RequiredApiDelegatedPermissions("sharepoint/AllSites.FullControl")] + [OutputType(typeof(MultiGeoCompanyAllowedDataLocation))] + public class GetMultiGeoCompanyAllowedDataLocation : PnPSharePointOnlineAdminCmdlet + { + protected override void ExecuteCmdlet() + { + var multiGeoRestApiClient = new MultiGeoRestApiClient(AdminContext); + WriteObject(multiGeoRestApiClient.GetAllowedDataLocations(), true); + } + } +} \ No newline at end of file diff --git a/src/Commands/Model/MultiGeoCompanyAllowedDataLocation.cs b/src/Commands/Model/MultiGeoCompanyAllowedDataLocation.cs new file mode 100644 index 000000000..1684924b3 --- /dev/null +++ b/src/Commands/Model/MultiGeoCompanyAllowedDataLocation.cs @@ -0,0 +1,23 @@ +namespace PnP.PowerShell.Commands.Model +{ + /// + /// Contains an allowed multi-geo data location configured for the SharePoint Online tenant. + /// + public class MultiGeoCompanyAllowedDataLocation + { + /// + /// The geo location code, such as NAM or EUR. + /// + public string Location { get; set; } + + /// + /// The SharePoint Online domain associated with the geo location. + /// + public string Domain { get; set; } + + /// + /// Indicates whether this is the tenant default data location. + /// + public bool IsDefault { get; set; } + } +} \ No newline at end of file diff --git a/src/Commands/Utilities/MultiGeo/MultiGeoRestApiClient.cs b/src/Commands/Utilities/MultiGeo/MultiGeoRestApiClient.cs index a016b662c..1cf5531f2 100644 --- a/src/Commands/Utilities/MultiGeo/MultiGeoRestApiClient.cs +++ b/src/Commands/Utilities/MultiGeo/MultiGeoRestApiClient.cs @@ -16,12 +16,16 @@ namespace PnP.PowerShell.Commands.Utilities.MultiGeo internal class MultiGeoRestApiClient { private const string TenantRenameApiVersion = "1.5.3"; + private const string TenantRenameCancelApiVersion = "1.5.5"; private const string TenantRenameStatusV2ApiVersion = "1.5.18"; private const string TenantRenameJobsPath = "TenantRenameJobs"; private const string TenantRenameJobsPathToGetWarningMessages = "TenantRenameJobs/GetWarningMessages"; private const string TenantRenameJobsPathToGetStatus = "TenantRenameJobs/Get"; private const string TenantRenameJobsPathToGetStatusV2 = "TenantRenameJobs/GetV2"; private const string TenantRenameJobsPathToCancelAJob = "TenantRenameJobs/Cancel"; + private const string AllowedDataLocationsApiVersion = "1.3.11"; + private const string AllowedDataLocationsPath = "AllowedDataLocations"; + private const int MaximumPagination = 10; private static readonly TimeSpan CreateTenantRenameJobTimeout = TimeSpan.FromSeconds(300); private static readonly JsonSerializerOptions SerializerOptions = new() { @@ -54,12 +58,17 @@ internal TenantRenameJob GetTenantRenameJobV2() internal IEnumerable GetTenantRenameWarningMessages() { - return Get>(TenantRenameJobsPathToGetWarningMessages); + return GetFeed(TenantRenameJobsPathToGetWarningMessages, TenantRenameApiVersion); + } + + internal IEnumerable GetAllowedDataLocations() + { + return GetFeed(AllowedDataLocationsPath, AllowedDataLocationsApiVersion); } internal void CancelTenantRenameJob() { - Post(TenantRenameJobsPathToCancelAJob, payload: null); + Post(TenantRenameJobsPathToCancelAJob, payload: null, apiVersion: TenantRenameCancelApiVersion); } private T Get(string path, string apiVersion = TenantRenameApiVersion) @@ -68,6 +77,43 @@ private T Get(string path, string apiVersion = TenantRenameApiVersion) return DeserializeResponse(responseText); } + private IEnumerable GetFeed(string path, string apiVersion) + { + var results = new List(); + var requestUri = CreateApiUri(path, apiVersion); + var pages = 0; + + while (requestUri != null && pages < MaximumPagination) + { + var responseText = Send(() => CreateRequest(HttpMethod.Get, requestUri), timeout: null, allowRetries: true); + var collection = DeserializeFeed(responseText); + if (collection.Value != null) + { + results.AddRange(collection.Value); + } + + if (!string.IsNullOrWhiteSpace(collection.NextLink)) + { + requestUri = new Uri(requestUri, collection.NextLink); + checked + { + pages++; + } + } + else + { + requestUri = null; + } + } + + if (requestUri != null) + { + throw new InvalidOperationException("SharePoint Online REST request returned too many pages."); + } + + return results; + } + private T Post(string path, object payload, TimeSpan? timeout = null, string apiVersion = TenantRenameApiVersion) { var jsonPayload = payload == null ? null : JsonSerializer.Serialize(payload, SerializerOptions); @@ -77,7 +123,12 @@ private T Post(string path, object payload, TimeSpan? timeout = null, string private HttpRequestMessage CreateRequest(HttpMethod method, string path, string apiVersion, string jsonPayload = null) { - var request = new HttpRequestMessage(method, CreateApiUri(path, apiVersion)) + return CreateRequest(method, CreateApiUri(path, apiVersion), jsonPayload); + } + + private HttpRequestMessage CreateRequest(HttpMethod method, Uri requestUri, string jsonPayload = null) + { + var request = new HttpRequestMessage(method, requestUri) { Version = new Version(2, 0) }; @@ -170,6 +221,59 @@ private static T DeserializeResponse(string responseText) return JsonSerializer.Deserialize(responseElement.GetRawText(), SerializerOptions); } + private static ODataFeed DeserializeFeed(string responseText) + { + if (string.IsNullOrWhiteSpace(responseText)) + { + return new ODataFeed(); + } + + using var jsonDocument = JsonDocument.Parse(responseText); + var responseElement = jsonDocument.RootElement; + if (responseElement.ValueKind == JsonValueKind.Object && responseElement.TryGetProperty("d", out var dElement)) + { + responseElement = dElement; + } + + var feed = new ODataFeed(); + if (responseElement.ValueKind == JsonValueKind.Object) + { + if (responseElement.TryGetProperty("value", out var valueElement) || responseElement.TryGetProperty("results", out valueElement)) + { + feed.Value = DeserializeFeedValue(valueElement); + } + + feed.NextLink = GetStringProperty(responseElement, "@odata.nextLink", "odata.nextLink", "nextLink", "__next"); + return feed; + } + + feed.Value = DeserializeFeedValue(responseElement); + return feed; + } + + private static T[] DeserializeFeedValue(JsonElement valueElement) + { + if (valueElement.ValueKind != JsonValueKind.Array) + { + return Array.Empty(); + } + + return JsonSerializer.Deserialize(valueElement.GetRawText(), SerializerOptions) ?? Array.Empty(); + } + + private static string GetStringProperty(JsonElement element, params string[] propertyNames) + { + foreach (var propertyName in propertyNames) + { + if (element.TryGetProperty(propertyName, out var propertyElement) && propertyElement.ValueKind == JsonValueKind.String) + { + return propertyElement.GetString(); + } + } + + return null; + } + private static JsonElement UnwrapODataResponse(JsonElement responseElement) { if (responseElement.ValueKind != JsonValueKind.Object) @@ -240,5 +344,12 @@ private static bool TryGetODataErrorMessage(JsonElement rootElement, out string return false; } + + private sealed class ODataFeed + { + public T[] Value { get; set; } + + public string NextLink { get; set; } + } } } From 6cd4c9d4994fdddc1e1d5de1ae14684219129de7 Mon Sep 17 00:00:00 2001 From: Gautam Sheth Date: Sun, 31 May 2026 17:51:14 +0300 Subject: [PATCH 2/2] Update CHANGELOG to include Get-PnPMultiGeoCompanyAllowedDataLocation cmdlet --- CHANGELOG.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1abf01de1..53fec19d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,13 +8,19 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). ## [Current nightly] +### Added +- Added `Get-PnPMultiGeoCompanyAllowedDataLocation` cmdlet to retrieve SharePoint Online multi-geo allowed data locations. [#5336](https://github.com/pnp/powershell/pull/5336) + ### Changed -- Added properties `CoreOrganizationSharingLinkRecommendedExpirationInDays`, `CoreOrganizationSharingLinkMaxExpirationInDays`,`RestrictResourceAccountAccess`, `RestrictExternalSharingForAgents` to Set-pnptenant and Get-pnptenant cmdlet. [#5330](https://github.com/pnp/powershell/pull/5330) -- Added properties OrganizationSharingLinkRecommendedExpirationInDays, OrganizationSharingLinkMaxExpirationInDays, OverrideTenantOrganizationSharingLinkExpirationPolicy to set-pnpsite, set-pnptenantsite cmdlets. [#5333](https://github.com/pnp/powershell/pull/5333) +- Added properties `CoreOrganizationSharingLinkRecommendedExpirationInDays`, `CoreOrganizationSharingLinkMaxExpirationInDays`,`RestrictResourceAccountAccess`, `RestrictExternalSharingForAgents` to `Set-PnPTenant` and `Get-PnPTenant` cmdlet. [#5330](https://github.com/pnp/powershell/pull/5330) +- Added properties `OrganizationSharingLinkRecommendedExpirationInDays`, `OrganizationSharingLinkMaxExpirationInDays`, `OverrideTenantOrganizationSharingLinkExpirationPolicy` to `Set-PnPSite`, `Set-PnPTenantsite` cmdlets. [#5333](https://github.com/pnp/powershell/pull/5333) +- Added `WhoCanShareAllowListInTenantByPrincipalIdentity` property to `Set-PnPTenant` cmdlet. [#5322](https://github.com/pnp/powershell/pull/5322) ### Contributors - Reshmee Auckloo [reshmee011] +- [Tetronic] +- Vasco Azevedo [vascoazevedo08] ## [3.2.0]