From 5c6b19858608593493bd585fc8d10902f5a7a6bb Mon Sep 17 00:00:00 2001 From: Koen Zomers Date: Tue, 8 Feb 2022 15:44:01 +0100 Subject: [PATCH] Updates to `Sync-PnPSharePointUserProfilesFromAzureActiveDirectory` and `Get-PnPAzureADUser` (#1559) * Fixes for user profile sync * Fixes for user profile syncing * Fixes for user profile sync * Fixes for user profile syncing * Updating changelog * Adding PR reference * Making changelog entry shorter * Update CHANGELOG.md * Update CHANGELOG.md --- CHANGELOG.md | 4 ++ src/Commands/AzureAD/GetAzureADUser.cs | 13 ++-- ...rProfileImportProfilePropertiesJobError.cs | 16 +++++ ...rProfileImportProfilePropertiesJobState.cs | 43 ++++++++++++ src/Commands/Files/AddFile.cs | 33 +++++++-- .../SharePointUserProfileSyncStatus.cs | 68 +++++++++++++++++++ src/Commands/Utilities/AzureAdUtility.cs | 28 ++++---- .../Utilities/SharePointUserProfileSync.cs | 17 ++++- 8 files changed, 194 insertions(+), 28 deletions(-) create mode 100644 src/Commands/Enums/SharePointUserProfileImportProfilePropertiesJobError.cs create mode 100644 src/Commands/Enums/SharePointUserProfileImportProfilePropertiesJobState.cs create mode 100644 src/Commands/Model/SharePointUserProfileSync/SharePointUserProfileSyncStatus.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index b113f26e5..16af1184d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). - Added `Add\Remove\Set-PnPAdaptiveScopeProperty` cmdlets to add/update/remove a property bag value while dealing with the noscript toggling in one cmdlet [#1556](https://github.com/pnp/powershell/pull/1556) - Added support to add multiple owners and members in `New-PnPTeamsTeam` cmdlet [#1241](https://github.com/pnp/powershell/pull/1241) - Added the ability to set the title of a new modern page in SharePoint Online using `Add-PnPPage` to be different from its filename by using `-Title` +- Added optional `-UseBeta` parameter to `Get-PnPAzureADUser` to force it to use the Microsoft Graph beta endpoint. This can be necessary when i.e. using `-Select "PreferredDataLocation"` to query for users with a specific multi geo location as this property is only available through the beta endpoint. [#1559](https://github.com/pnp/powershell/pull/1559) +- Added `-Content` option to `Add-PnPFile` which allows creating a new file on SharePoint Online and directly providing its textual content, i.e. to upload a log file of the execution [#1559](https://github.com/pnp/powershell/pull/1559) - Added `Get-PnPTeamsPrimaryChannel` to get the primary Teams channel, general, of a Team [#1572](https://github.com/pnp/powershell/pull/1572) - Added `Publish\Unpublish-PnPContentType` to allow for content types to be published or unpublished on hub sites [#1597](https://github.com/pnp/powershell/pull/1597) - Added `Get-PnPContentTypePublishingStatus` to get te current publication state of a content type in the content type hub site [#1597](https://github.com/pnp/powershell/pull/1597) @@ -31,6 +33,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). - Improved `Add-PnPTeamsUser` cmdlet. The cmdlet executes faster and we can now add users in batches of 200. [#1548](https://github.com/pnp/powershell/pull/1548) - The `Move\Remove\Rename-PnPFolder` cmdlets now support pipebinds. - Changed `Add-PnPDataRowsToSiteTemplate`, it will return a warning if user(s) are not found during list item extraction. Earlier it used to throw error and stop extraction of list items. +- Changed the return type of `Sync-PnPSharePointUserProfilesFromAzureActiveDirectory` to return our own entity instead of the one returned by CSOM [#1559](https://github.com/pnp/powershell/pull/1559) +- Changed running `Sync-PnPSharePointUserProfilesFromAzureActiveDirectory` with `-WhatIf` to also provide a return entity providing the path to where the JSON file has been uploaded to [#1559](https://github.com/pnp/powershell/pull/1559) - Disabling telemetry collection now requires either setting the environment variable or creating the telemetry file ([documentation](https://pnp.github.io/powershell/articles/configuration.html)) [#1504](https://github.com/pnp/powershell/pull/1504) - Changed `Get-PnPAzureADUser` to now return all the users in Azure Active Directory by default, instead of only the first 999, unless you specified `-EndIndex:$null` [#1565](https://github.com/pnp/powershell/pull/1565) - Changed `Get-PnPTenantDeletedSite -Identity` no longer returning an unknown exception when no site collection with the provided Url exists in the tenant recycle bin but instead returning no output to align with other cmdlets [#1596](https://github.com/pnp/powershell/pull/1596) diff --git a/src/Commands/AzureAD/GetAzureADUser.cs b/src/Commands/AzureAD/GetAzureADUser.cs index 7d4fcc019..bf2719f1e 100644 --- a/src/Commands/AzureAD/GetAzureADUser.cs +++ b/src/Commands/AzureAD/GetAzureADUser.cs @@ -45,6 +45,11 @@ public class GetAzureADUser : PnPGraphCmdlet [Parameter(Mandatory = false, ParameterSetName = ParameterSet_DELTA)] public int? EndIndex = null; + [Parameter(Mandatory = false, ParameterSetName = ParameterSet_BYID)] + [Parameter(Mandatory = false, ParameterSetName = ParameterSet_LIST)] + [Parameter(Mandatory = false, ParameterSetName = ParameterSet_DELTA)] + public SwitchParameter UseBeta; + protected override void ExecuteCmdlet() { if (PnPConnection.Current.ClientId == PnPConnection.PnPManagementShellClientId) @@ -56,22 +61,22 @@ protected override void ExecuteCmdlet() PnP.PowerShell.Commands.Model.AzureAD.User user; if (Guid.TryParse(Identity, out Guid identityGuid)) { - user = PnP.PowerShell.Commands.Utilities.AzureAdUtility.GetUser(AccessToken, identityGuid); + user = PnP.PowerShell.Commands.Utilities.AzureAdUtility.GetUser(AccessToken, identityGuid, useBetaEndPoint: UseBeta.IsPresent); } else { - user = PnP.PowerShell.Commands.Utilities.AzureAdUtility.GetUser(AccessToken, WebUtility.UrlEncode(Identity), Select); + user = PnP.PowerShell.Commands.Utilities.AzureAdUtility.GetUser(AccessToken, WebUtility.UrlEncode(Identity), Select, useBetaEndPoint: UseBeta.IsPresent); } WriteObject(user); } else if (ParameterSpecified(nameof(Delta))) { - var userDelta = PnP.PowerShell.Commands.Utilities.AzureAdUtility.ListUserDelta(AccessToken, DeltaToken, Filter, OrderBy, Select, StartIndex, EndIndex); + var userDelta = PnP.PowerShell.Commands.Utilities.AzureAdUtility.ListUserDelta(AccessToken, DeltaToken, Filter, OrderBy, Select, StartIndex, EndIndex, useBetaEndPoint: UseBeta.IsPresent); WriteObject(userDelta); } else { - var users = PnP.PowerShell.Commands.Utilities.AzureAdUtility.ListUsers(AccessToken, Filter, OrderBy, Select, StartIndex, EndIndex); + var users = PnP.PowerShell.Commands.Utilities.AzureAdUtility.ListUsers(AccessToken, Filter, OrderBy, Select, StartIndex, EndIndex, useBetaEndPoint: UseBeta.IsPresent); WriteObject(users, true); } } diff --git a/src/Commands/Enums/SharePointUserProfileImportProfilePropertiesJobError.cs b/src/Commands/Enums/SharePointUserProfileImportProfilePropertiesJobError.cs new file mode 100644 index 000000000..b2f099c47 --- /dev/null +++ b/src/Commands/Enums/SharePointUserProfileImportProfilePropertiesJobError.cs @@ -0,0 +1,16 @@ +namespace PnP.PowerShell.Commands.Enums +{ + /// + /// Types of errors that can occur while performing a SharePoint Online User Profile Import + /// + public enum SharePointUserProfileImportProfilePropertiesJobError + { + NoError = 0, + InternalError = 1, + DataFileNotExist = 20, + DataFileNotInTenant = 21, + DataFileTooBig = 22, + InvalidDataFile = 23, + ImportCompleteWithError = 30 + } +} \ No newline at end of file diff --git a/src/Commands/Enums/SharePointUserProfileImportProfilePropertiesJobState.cs b/src/Commands/Enums/SharePointUserProfileImportProfilePropertiesJobState.cs new file mode 100644 index 000000000..35a538c74 --- /dev/null +++ b/src/Commands/Enums/SharePointUserProfileImportProfilePropertiesJobState.cs @@ -0,0 +1,43 @@ +namespace PnP.PowerShell.Commands.Enums +{ + /// + /// The states a SharePoint Online User Profile import job can be in + /// + public enum SharePointUserProfileImportProfilePropertiesJobState + { + /// + /// State is unknown + /// + Unknown = 0, + + /// + /// The file has been submitted to SharePoint Online for processing + /// + Submitted = 1, + + /// + /// The file is currently being processed to validate if it can be used + /// + Processing = 2, + + /// + /// The file is queued and being executed + /// + Queued = 3, + + /// + /// The import process has completed successfully + /// + Succeeded = 4, + + /// + /// The import process has failed to complete + /// + Error = 5, + + /// + /// The import process will not start + /// + WontStart = 99 + } +} \ No newline at end of file diff --git a/src/Commands/Files/AddFile.cs b/src/Commands/Files/AddFile.cs index 38c910577..54c393ae4 100644 --- a/src/Commands/Files/AddFile.cs +++ b/src/Commands/Files/AddFile.cs @@ -13,6 +13,7 @@ public class AddFile : PnPWebCmdlet { private const string ParameterSet_ASFILE = "Upload file"; private const string ParameterSet_ASSTREAM = "Upload file from stream"; + private const string ParameterSet_ASTEXT = "Upload file from text"; [Parameter(Mandatory = true, ParameterSetName = ParameterSet_ASFILE)] [ValidateNotNullOrEmpty] @@ -23,6 +24,7 @@ public class AddFile : PnPWebCmdlet public FolderPipeBind Folder; [Parameter(Mandatory = true, ParameterSetName = ParameterSet_ASSTREAM)] + [Parameter(Mandatory = true, ParameterSetName = ParameterSet_ASTEXT)] [ValidateNotNullOrEmpty] public string FileName = string.Empty; @@ -34,6 +36,9 @@ public class AddFile : PnPWebCmdlet [ValidateNotNullOrEmpty] public Stream Stream; + [Parameter(Mandatory = true, ParameterSetName = ParameterSet_ASTEXT)] + public string Content; + [Parameter(Mandatory = false)] public SwitchParameter Checkout; @@ -112,14 +117,30 @@ protected override void ExecuteCmdlet() { // Swallow exception, file does not exist } } + Microsoft.SharePoint.Client.File file; - if (ParameterSetName == ParameterSet_ASFILE) - { - file = folder.UploadFile(FileName, Path, true); - } - else + switch (ParameterSetName) { - file = folder.UploadFile(FileName, Stream, true); + case ParameterSet_ASFILE: + file = folder.UploadFile(FileName, Path, true); + break; + + case ParameterSet_ASTEXT: + using (var stream = new MemoryStream()) + { + using (var writer = new StreamWriter(stream)) + { + writer.Write(Content); + writer.Flush(); + stream.Position = 0; + file = folder.UploadFile(FileName, stream, true); + } + } + break; + + default: + file = folder.UploadFile(FileName, Stream, true); + break; } bool updateRequired = false; diff --git a/src/Commands/Model/SharePointUserProfileSync/SharePointUserProfileSyncStatus.cs b/src/Commands/Model/SharePointUserProfileSync/SharePointUserProfileSyncStatus.cs new file mode 100644 index 000000000..05b28cb94 --- /dev/null +++ b/src/Commands/Model/SharePointUserProfileSync/SharePointUserProfileSyncStatus.cs @@ -0,0 +1,68 @@ +using System; +using Microsoft.Online.SharePoint.TenantManagement; +using PnP.PowerShell.Commands.Enums; + +namespace PnP.PowerShell.Commands.Model.SharePoint.SharePointUserProfileSync +{ + /// + /// Contains the status of a SharePoint Online User Profile Import job + /// + public class SharePointUserProfileSyncStatus + { + #region Properties + + /// + /// Details on the type of error that occurred, if any + /// + public SharePointUserProfileImportProfilePropertiesJobError Error { get; set; } + + /// + /// The error message, if an error occurred + /// + public string ErrorMessage { get; set; } + + /// + /// Unique identifier of the import job + /// + public Guid? JobId { get; set; } + + /// + /// + /// + public string LogFolderUri { get; set; } + + /// + /// + /// + public string SourceUri { get; set; } + + /// + /// State the user profile import process is in + /// + public SharePointUserProfileImportProfilePropertiesJobState State { get; set; } + + #endregion + + #region Methods + + /// + /// Takes an instance of ImportProfilePropertiesJobInfo from CSOM and maps it to a local SharePointUserProfileSyncStatus entity + /// + /// Instance to map from + public static SharePointUserProfileSyncStatus ParseFromImportProfilePropertiesJobInfo(ImportProfilePropertiesJobInfo importProfilePropertiesJobInfo) + { + var result = new SharePointUserProfileSyncStatus + { + Error = Enum.TryParse(importProfilePropertiesJobInfo.Error.ToString(), out SharePointUserProfileImportProfilePropertiesJobError sharePointUserProfileImportProfilePropertiesJobError) ? sharePointUserProfileImportProfilePropertiesJobError : SharePointUserProfileImportProfilePropertiesJobError.NoError, + ErrorMessage = importProfilePropertiesJobInfo.ErrorMessage, + JobId = importProfilePropertiesJobInfo.JobId, + LogFolderUri = importProfilePropertiesJobInfo.LogFolderUri, + SourceUri = importProfilePropertiesJobInfo.SourceUri, + State = Enum.TryParse(importProfilePropertiesJobInfo.State.ToString(), out SharePointUserProfileImportProfilePropertiesJobState sharePointUserProfileImportProfilePropertiesJobState) ? sharePointUserProfileImportProfilePropertiesJobState : SharePointUserProfileImportProfilePropertiesJobState.Unknown + }; + return result; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Commands/Utilities/AzureAdUtility.cs b/src/Commands/Utilities/AzureAdUtility.cs index 6f0ef9ac8..0a79f147d 100644 --- a/src/Commands/Utilities/AzureAdUtility.cs +++ b/src/Commands/Utilities/AzureAdUtility.cs @@ -22,10 +22,11 @@ internal static class AzureAdUtility /// Optional additional properties to fetch for the users /// Optional start index indicating starting from which result to start returning users /// Optional end index indicating up to which result to return users. By default all users will be returned. + /// Indicates if the v1.0 (false) or beta (true) endpoint should be used at Microsoft Graph to query for the data /// UserDelta instance - public static UserDelta ListUserDelta(string accessToken, string deltaToken, string filter, string orderby, string[] selectProperties = null, int startIndex = 0, int? endIndex = null) + public static UserDelta ListUserDelta(string accessToken, string deltaToken, string filter, string orderby, string[] selectProperties = null, int startIndex = 0, int? endIndex = null, bool useBetaEndPoint = false) { - var userDelta = PnP.Framework.Graph.UsersUtility.ListUserDelta(accessToken, deltaToken, filter, orderby, selectProperties, startIndex, endIndex); + var userDelta = PnP.Framework.Graph.UsersUtility.ListUserDelta(accessToken, deltaToken, filter, orderby, selectProperties, startIndex, endIndex, useBetaEndPoint: useBetaEndPoint); var result = new UserDelta { @@ -45,28 +46,26 @@ public static UserDelta ListUserDelta(string accessToken, string deltaToken, str /// Allows providing the names of properties to return regarding the users. If not provided, the standard properties will be returned. /// First item in the results returned by Microsoft Graph to return /// Last item in the results returned by Microsoft Graph to return. Provide NULL to return all results that exist. - /// Number of times to retry the request in case of throttling - /// Milliseconds to wait before retrying the request. The delay will be increased (doubled) every retry. + /// Indicates if the v1.0 (false) or beta (true) endpoint should be used at Microsoft Graph to query for the data /// List with User objects - public static List ListUsers(string accessToken, string filter, string orderby, string[] selectProperties = null, int startIndex = 0, int? endIndex = 999) + public static List ListUsers(string accessToken, string filter, string orderby, string[] selectProperties = null, int startIndex = 0, int? endIndex = 999, bool useBetaEndPoint = false) { - return PnP.Framework.Graph.UsersUtility.ListUsers(accessToken, filter, orderby, selectProperties, startIndex, endIndex).Select(User.CreateFrom).ToList(); + return PnP.Framework.Graph.UsersUtility.ListUsers(accessToken, filter, orderby, selectProperties, startIndex, endIndex, useBetaEndPoint: useBetaEndPoint).Select(User.CreateFrom).ToList(); } /// - /// Returns the user with the provided userId from Azure Active Directory + /// Returns the user with the provided from Azure Active Directory /// /// The OAuth 2.0 Access Token to use for invoking the Microsoft Graph /// The unique identifier of the user in Azure Active Directory to return /// Allows providing the names of properties to return regarding the users. If not provided, the standard properties will be returned. /// First item in the results returned by Microsoft Graph to return /// Last item in the results returned by Microsoft Graph to return. Provide NULL to return all results that exist. - /// Number of times to retry the request in case of throttling - /// Milliseconds to wait before retrying the request. The delay will be increased (doubled) every retry. + /// Indicates if the v1.0 (false) or beta (true) endpoint should be used at Microsoft Graph to query for the data /// List with User objects - public static User GetUser(string accessToken, Guid userId, string[] selectProperties = null, int startIndex = 0, int? endIndex = 999) + public static User GetUser(string accessToken, Guid userId, string[] selectProperties = null, int startIndex = 0, int? endIndex = 999, bool useBetaEndPoint = false) { - return PnP.Framework.Graph.UsersUtility.ListUsers(accessToken, $"id eq '{userId}'", null, selectProperties, startIndex, endIndex).Select(User.CreateFrom).FirstOrDefault(); + return PnP.Framework.Graph.UsersUtility.ListUsers(accessToken, $"id eq '{userId}'", null, selectProperties, startIndex, endIndex, useBetaEndPoint: useBetaEndPoint).Select(User.CreateFrom).FirstOrDefault(); } /// @@ -77,12 +76,11 @@ public static User GetUser(string accessToken, Guid userId, string[] selectPrope /// Allows providing the names of properties to return regarding the users. If not provided, the standard properties will be returned. /// First item in the results returned by Microsoft Graph to return /// Last item in the results returned by Microsoft Graph to return. Provide NULL to return all results that exist. - /// Number of times to retry the request in case of throttling - /// Milliseconds to wait before retrying the request. The delay will be increased (doubled) every retry. + /// Indicates if the v1.0 (false) or beta (true) endpoint should be used at Microsoft Graph to query for the data /// User object - public static User GetUser(string accessToken, string userPrincipalName, string[] selectProperties = null, int startIndex = 0, int? endIndex = 999) + public static User GetUser(string accessToken, string userPrincipalName, string[] selectProperties = null, int startIndex = 0, int? endIndex = 999, bool useBetaEndPoint = false) { - return PnP.Framework.Graph.UsersUtility.ListUsers(accessToken, $"userPrincipalName eq '{userPrincipalName}'", null, selectProperties, startIndex, endIndex).Select(User.CreateFrom).FirstOrDefault(); + return PnP.Framework.Graph.UsersUtility.ListUsers(accessToken, $"userPrincipalName eq '{userPrincipalName}'", null, selectProperties, startIndex, endIndex, useBetaEndPoint: useBetaEndPoint).Select(User.CreateFrom).FirstOrDefault(); } #endregion diff --git a/src/Commands/Utilities/SharePointUserProfileSync.cs b/src/Commands/Utilities/SharePointUserProfileSync.cs index 6825a7fb3..6e2df9bde 100644 --- a/src/Commands/Utilities/SharePointUserProfileSync.cs +++ b/src/Commands/Utilities/SharePointUserProfileSync.cs @@ -8,6 +8,7 @@ using PnP.Framework.Utilities; using System.Threading.Tasks; using System.Reflection; +using PnP.PowerShell.Commands.Model.SharePoint.SharePointUserProfileSync; namespace PnP.PowerShell.Commands.Utilities { @@ -24,7 +25,8 @@ public static class SharePointUserProfileSync /// Hashtable with the mapping from the Azure Active Directory property (the value) to the SharePoint Online User Profile Property (the key) /// Location in the currently connected to site where to upload the JSON file to with instructions to update the user profiles /// Boolean indicating if only the mappings file should be created and uploaded to SharePoint Online (true) or if the import job on that file should also be invoked (false) - public static async Task SyncFromAzureActiveDirectory(ClientContext clientContext, IEnumerable users, Hashtable userProfilePropertyMappings, string sharePointFolder, bool onlyCreateAndUploadMappingsFile = false) + /// Information on the status of the import job that has been created because of this action + public static async Task SyncFromAzureActiveDirectory(ClientContext clientContext, IEnumerable users, Hashtable userProfilePropertyMappings, string sharePointFolder, bool onlyCreateAndUploadMappingsFile = false) { var webServerRelativeUrl = clientContext.Web.EnsureProperty(w => w.ServerRelativeUrl); if (!sharePointFolder.ToLower().StartsWith(webServerRelativeUrl)) @@ -109,7 +111,14 @@ public static async Task SyncFromAzureActiveDire } // Check if we should kick off the process to import the file - if(onlyCreateAndUploadMappingsFile) return null; + if (onlyCreateAndUploadMappingsFile) + { + return new SharePointUserProfileSyncStatus + { + SourceUri = new Uri(clientContext.Url).GetLeftPart(UriPartial.Authority) + file.ServerRelativeUrl, + State = Enums.SharePointUserProfileImportProfilePropertiesJobState.WontStart + }; + } // Instruct SharePoint Online to process the JSON file var o365 = new Office365Tenant(clientContext); @@ -123,7 +132,9 @@ public static async Task SyncFromAzureActiveDire clientContext.Load(job); clientContext.ExecuteQueryRetry(); - return job; + // Map the CSOM result object to our own entity + var sharePointUserProfileSyncStatus = SharePointUserProfileSyncStatus.ParseFromImportProfilePropertiesJobInfo(job); + return sharePointUserProfileSyncStatus; } } } \ No newline at end of file