diff --git a/Binaries/SharePointPnP.Modernization.Framework.dll b/Binaries/SharePointPnP.Modernization.Framework.dll index 6cd528dbf..4c1019ef7 100644 Binary files a/Binaries/SharePointPnP.Modernization.Framework.dll and b/Binaries/SharePointPnP.Modernization.Framework.dll differ diff --git a/Binaries/release/SharePointPnP.Modernization.Framework.dll b/Binaries/release/SharePointPnP.Modernization.Framework.dll index f4724d2d9..4b3804fa2 100644 Binary files a/Binaries/release/SharePointPnP.Modernization.Framework.dll and b/Binaries/release/SharePointPnP.Modernization.Framework.dll differ diff --git a/CHANGELOG.md b/CHANGELOG.md index b5e0098f5..b3b34297b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,37 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). +## [3.26.2010.0 - unreleased] + +### Added +- Added `Set-PnPTenant -EnableAutoNewsDigest` option to disable the automatic news digest mails sent to end users [PR #2901](https://github.com/pnp/PnP-PowerShell/pull/2901) +- Added `Register-PnPManagementShellAccess` cmdlet to register correct access required for the multi-tenant application PnP Management Shell which is used behind the scenes of the Provisioning Engine for certain actions towards SharePoint Online. +- Added the description being shown of a Site Script when running `Get-PnPSiteScript` [PR #2895](https://github.com/pnp/PnP-PowerShell/pull/2895) +- Added -Includes to `Get-PnPUser` [PR #2939](https://github.com/pnp/PnP-PowerShell/pull/2939) +- Added an sample for Azure Automation [PR #2835](https://github.com/pnp/PnP-PowerShell/pull/2835) +- Added -DisableCustomAppAuthentication to `Set-PnPTenant` [PR #2923](https://github.com/pnp/PnP-PowerShell/pull/2923) + +### Changed +- Fixed example for `Get-PnPTeamsChannel` [PR #2919](https://github.com/pnp/PnP-PowerShell/pull/2919) +- Fixed possible token acquisition issue when extracting a tenant template. [PR #2874](https://github.com/pnp/PnP-PowerShell/pull/2874) +- Fixed scenario when no configuration file provided with Get-PnPTenantTemplate throws an undefined error [PR #2873](https://github.com/pnp/PnP-PowerShell/pull/2873) +- Fixed examples for `New-PnPTeamsTeam` [PR #2870](https://github.com/pnp/PnP-PowerShell/pull/2870) +- Updated documentation for `Get-PnPField` [PR #2856](https://github.com/pnp/PnP-PowerShell/pull/2856) +- Updated documentation for `Get-PnPListItem` [PR #2806](https://github.com/pnp/PnP-PowerShell/pull/2806) +- Improved certificate file handling and errors shown if the file does not exist, is empty or points to a folder [PR #2888](https://github.com/pnp/PnP-PowerShell/pull/2888) +- Fixed issue with where connecting with Connect-PnPOnline to an URL ending on a slash could cause an exception when using some cmdlets. + +### Contributors +- Koen Zomers [koenzomers] +- Gautam Sheth [gautamdsheth] +- Giacomo Pozzoni [jackpoz] +- James May [fowl2] +- Aimery Thomas [a1mery] +- Heinrich Ulbricht [heinrich-ulbricht] +- Veronique Lengelle [veronicageek] +- Todd Klindt [ToddKlindt] +- Paul Bullock [pkbullock] + ## [3.25.2009.1] ### Changed diff --git a/Commands/Admin/GetStorageEntity.cs b/Commands/Admin/GetStorageEntity.cs index 8b7a654dc..23261120b 100644 --- a/Commands/Admin/GetStorageEntity.cs +++ b/Commands/Admin/GetStorageEntity.cs @@ -11,7 +11,7 @@ namespace PnP.PowerShell.Commands { - [Cmdlet(VerbsCommon.Get, "PnPStorageEntity", SupportsShouldProcess = true)] + [Cmdlet(VerbsCommon.Get, "PnPStorageEntity")] [CmdletHelp(@"Retrieve Storage Entities / Farm Properties from either the Tenant App Catalog or from the current site if it has a site scope app catalog.", Category = CmdletHelpCategory.TenantAdmin, SupportedPlatform = CmdletSupportedPlatform.Online)] diff --git a/Commands/Admin/GetTenantAppCatalogUrl.cs b/Commands/Admin/GetTenantAppCatalogUrl.cs index bf76f233e..860e77a7b 100644 --- a/Commands/Admin/GetTenantAppCatalogUrl.cs +++ b/Commands/Admin/GetTenantAppCatalogUrl.cs @@ -5,7 +5,7 @@ namespace PnP.PowerShell.Commands { - [Cmdlet(VerbsCommon.Get, "PnPTenantAppCatalogUrl", SupportsShouldProcess = true)] + [Cmdlet(VerbsCommon.Get, "PnPTenantAppCatalogUrl")] [CmdletHelp(@"Retrieves the url of the tenant scoped app catalog", Category = CmdletHelpCategory.TenantAdmin, SupportedPlatform = CmdletSupportedPlatform.Online)] diff --git a/Commands/Admin/GetTenantSite.cs b/Commands/Admin/GetTenantSite.cs index 64170c53b..b0e751dce 100644 --- a/Commands/Admin/GetTenantSite.cs +++ b/Commands/Admin/GetTenantSite.cs @@ -11,7 +11,7 @@ namespace PnP.PowerShell.Commands { - [Cmdlet(VerbsCommon.Get, "PnPTenantSite", SupportsShouldProcess = true)] + [Cmdlet(VerbsCommon.Get, "PnPTenantSite")] [CmdletHelp(@"Retrieve site information.", "Use this cmdlet to retrieve site information from your tenant administration.", Category = CmdletHelpCategory.TenantAdmin, SupportedPlatform = CmdletSupportedPlatform.SP2016 | CmdletSupportedPlatform.SP2019 | CmdletSupportedPlatform.Online, diff --git a/Commands/Admin/SetTenant.cs b/Commands/Admin/SetTenant.cs index 20d0edebe..094fd4fdf 100644 --- a/Commands/Admin/SetTenant.cs +++ b/Commands/Admin/SetTenant.cs @@ -405,6 +405,12 @@ The only two characters that can be managed at this time are the # and % charact [Parameter(Mandatory = false, HelpMessage = "Boolean indicating if Azure Information Protection (AIP) should be enabled on the tenant. For more information, see https://docs.microsoft.com/microsoft-365/compliance/sensitivity-labels-sharepoint-onedrive-files#use-powershell-to-enable-support-for-sensitivity-labels")] public bool? EnableAIPIntegration; + + [Parameter(Mandatory = false)] + public bool? DisableCustomAppAuthentication; + + [Parameter(Mandatory = false, HelpMessage = "Boolean indicating if a news digest should automatically be sent to end users to inform them about news that they may have missed. On by default. For more information, see https://aka.ms/autonewsdigest")] + public bool? EnableAutoNewsDigest; protected override void ExecuteCmdlet() { @@ -972,6 +978,16 @@ protected override void ExecuteCmdlet() Tenant.EnableAIPIntegration = EnableAIPIntegration.Value; isDirty = true; } + if (DisableCustomAppAuthentication.HasValue) + { + Tenant.DisableCustomAppAuthentication = DisableCustomAppAuthentication.Value; + isDirty = true; + } + if (EnableAutoNewsDigest.HasValue) + { + Tenant.EnableAutoNewsDigest = EnableAutoNewsDigest.Value; + isDirty = true; + } if (isDirty) { ClientContext.ExecuteQueryRetry(); diff --git a/Commands/Admin/SetTenantSyncClientRestriction.cs b/Commands/Admin/SetTenantSyncClientRestriction.cs index 16d550336..b3424441a 100644 --- a/Commands/Admin/SetTenantSyncClientRestriction.cs +++ b/Commands/Admin/SetTenantSyncClientRestriction.cs @@ -41,7 +41,7 @@ public class SetTenantSyncClientRestriction : PnPAdminCmdlet [Parameter(Mandatory = false, HelpMessage = @"Blocks certain file types from syncing with the new sync client (OneDrive.exe). Provide as one string separating the extensions using a semicolon (;). I.e. ""docx;pptx""")] public List ExcludedFileExtensions; - [Parameter(Mandatory = false, HelpMessage = "Controls whether or not a tenant's users can sync OneDrive for Business libraries with the old OneDrive for Business sync client. The valid values are OptOut, HardOptin, and SoftOptin.")] + [Parameter(Mandatory = false, HelpMessage = "Controls whether or not a tenant's users can sync OneDrive for Business libraries with the old OneDrive for Business sync client. The valid values are OptOut, HardOptin, and SoftOptin. GrooveBlockOption is planned to be deprecated. Please refrain from using the parameter.")] public Enums.GrooveBlockOption GrooveBlockOption; protected override void ExecuteCmdlet() @@ -90,4 +90,4 @@ protected override void ExecuteCmdlet() } } } -#endif \ No newline at end of file +#endif diff --git a/Commands/Base/ConnectOnline.cs b/Commands/Base/ConnectOnline.cs index 95c566929..2b9e93a36 100644 --- a/Commands/Base/ConnectOnline.cs +++ b/Commands/Base/ConnectOnline.cs @@ -651,6 +651,11 @@ protected override void ProcessRecord() /// protected void Connect() { + if (!string.IsNullOrEmpty(Url) && Url.EndsWith("/")) + { + Url = Url.Substring(0, Url.Length - 1); + } + PnPConnection connection = null; var latestVersion = PnPConnectionHelper.GetLatestVersion(); diff --git a/Commands/Base/InitializePowerShellAuthentication.cs b/Commands/Base/InitializePowerShellAuthentication.cs index cbc230fca..101b11644 100644 --- a/Commands/Base/InitializePowerShellAuthentication.cs +++ b/Commands/Base/InitializePowerShellAuthentication.cs @@ -250,24 +250,16 @@ protected override void ProcessRecord() record.Properties.Add(new PSVariableProperty(new PSVariable("AzureAppId", azureApp.AppId))); var waitTime = 60; - Host.UI.Write(ConsoleColor.Yellow, Host.UI.RawUI.BackgroundColor, $"Waiting {waitTime} seconds to launch consent flow in a browser window. This wait is required to make sure that Azure AD is able to initialize all required artifacts."); - - Console.TreatControlCAsInput = true; + Host.UI.Write(ConsoleColor.Yellow, Host.UI.RawUI.BackgroundColor, $"Waiting {waitTime} seconds to start the consent flow in a browser window. This wait is required to make sure that Azure AD is able to initialize all required artifacts."); for (var i = 0; i < waitTime; i++) { Host.UI.Write(ConsoleColor.Yellow, Host.UI.RawUI.BackgroundColor, "."); System.Threading.Thread.Sleep(1000); - // Check if CTRL+C has been pressed and if so, abort the wait - if (Host.UI.RawUI.KeyAvailable) + if (Stopping) { - var key = Host.UI.RawUI.ReadKey(ReadKeyOptions.AllowCtrlC | ReadKeyOptions.NoEcho | ReadKeyOptions.IncludeKeyUp); - if ((key.ControlKeyState.HasFlag(ControlKeyStates.LeftCtrlPressed) || key.ControlKeyState.HasFlag(ControlKeyStates.RightCtrlPressed)) && key.VirtualKeyCode == 67) - { - - break; - } + break; } } Host.UI.WriteLine(); diff --git a/Commands/Base/RegisterPnPManagementShellAccess.cs b/Commands/Base/RegisterPnPManagementShellAccess.cs new file mode 100644 index 000000000..a2dc165af --- /dev/null +++ b/Commands/Base/RegisterPnPManagementShellAccess.cs @@ -0,0 +1,37 @@ +using Microsoft.Identity.Client; +using OfficeDevPnP.Core; +using PnP.PowerShell.CmdletHelpAttributes; +using PnP.PowerShell.Commands.Model; +using System; +using System.Management.Automation; + +namespace PnP.PowerShell.Commands.Base +{ + [Cmdlet(VerbsLifecycle.Register, "PnPManagementShellAccess")] + [CmdletHelp("Registers the multi-tenant app PnP Management Shell for delegate access to the required environments", + Category = CmdletHelpCategory.TenantAdmin)] + [CmdletExample( + Code = "PS:> Register-PnPManagementShellAccess -SiteUrl https://yourtenant.sharepoint.com", + Remarks = "Will prompt you to authenticate and if needed will ask you to provide consent to specific required rights.", + SortOrder = 1)] + public class RegisterPnPManagementShellAccess : PSCmdlet + { + [Parameter(Mandatory = false)] + public AzureEnvironment AzureEnvironment = AzureEnvironment.Production; + + [Parameter(Mandatory = true)] + public string SiteUrl; + protected override void ProcessRecord() + { + var endPoint = GenericToken.GetAzureADLoginEndPoint(AzureEnvironment); + var uri = new Uri(SiteUrl); + var scopes = new[] { $"{uri.Scheme}://{uri.Authority}//.default" }; + + var application = PublicClientApplicationBuilder.Create(PnPConnection.PnPManagementShellClientId).WithAuthority($"{endPoint}/organizations/").WithRedirectUri("https://login.microsoftonline.com/common/oauth2/nativeclient").Build(); + + var result = application.AcquireTokenInteractive(scopes).ExecuteAsync().GetAwaiter().GetResult(); + result = application.AcquireTokenInteractive(new[] { "https://graph.microsoft.com/.default" }).ExecuteAsync().GetAwaiter().GetResult(); + + } + } +} \ No newline at end of file diff --git a/Commands/Base/TokenHandling.cs b/Commands/Base/TokenHandling.cs index 6bb277d98..9d2c62c5f 100644 --- a/Commands/Base/TokenHandling.cs +++ b/Commands/Base/TokenHandling.cs @@ -21,7 +21,7 @@ internal static string AcquireToken(string resource, string scope = null) if (scope == null) { // SharePoint or Graph V1 resource - var scopes = new[] { $"https://{resource}//.default" }; + var scopes = new[] { $"https://{resource.Replace("https://","").TrimEnd('/')}/.default" }; token = GenericToken.AcquireDelegatedTokenWithCredentials(PnPConnection.PnPManagementShellClientId, scopes, "https://login.microsoftonline.com/organizations/", PnPConnection.CurrentConnection.PSCredential.UserName, PnPConnection.CurrentConnection.PSCredential.Password); } else diff --git a/Commands/Fields/AddField.cs b/Commands/Fields/AddField.cs index 8eeac7a66..bd85eef3d 100644 --- a/Commands/Fields/AddField.cs +++ b/Commands/Fields/AddField.cs @@ -10,7 +10,7 @@ namespace PnP.PowerShell.Commands.Fields { [Cmdlet(VerbsCommon.Add, "PnPField", DefaultParameterSetName = "Add field to list")] [CmdletHelp("Add a field", - "Adds a field to a list or as a site column", + "Adds a field (a column) to a list or as a site column. To add a column of type Managed Metadata use the Add-PnPTaxonomyField cmdlet", Category = CmdletHelpCategory.Fields, OutputType = typeof(Field), OutputTypeLink = "https://docs.microsoft.com/en-us/previous-versions/office/sharepoint-server/ee545882(v=office.15)", diff --git a/Commands/Lists/GetListItem.cs b/Commands/Lists/GetListItem.cs index 57b63ffba..d77e644e8 100644 --- a/Commands/Lists/GetListItem.cs +++ b/Commands/Lists/GetListItem.cs @@ -43,7 +43,7 @@ namespace PnP.PowerShell.Commands.Lists Remarks = "Retrieves all list items from the Tasks list in pages of 1000 items", SortOrder = 7)] [CmdletExample( - Code = "PS:> Get-PnPListItem -List Tasks -PageSize 1000 -ScriptBlock { Param($items) $items.Context.ExecuteQuery() } | % { $_.BreakRoleInheritance($true, $true) }", + Code = "PS:> Get-PnPListItem -List Tasks -PageSize 1000 -ScriptBlock { Param($items) $items.Context.ExecuteQuery() } | ForEach-Object { $_.BreakRoleInheritance($true, $true) }", Remarks = "Retrieves all list items from the Tasks list in pages of 1000 items and breaks permission inheritance on each item", SortOrder = 8)] [CmdletExample( diff --git a/Commands/Model/GenericToken.cs b/Commands/Model/GenericToken.cs index f12372571..0786e41db 100644 --- a/Commands/Model/GenericToken.cs +++ b/Commands/Model/GenericToken.cs @@ -11,6 +11,8 @@ using System.Threading.Tasks; using System.Security; using OfficeDevPnP.Core; +using System.Management.Automation; +using PnP.PowerShell.Commands.Base; namespace PnP.PowerShell.Commands.Model { @@ -181,11 +183,28 @@ public static GenericToken AcquireApplicationToken(string tenant, string clientI try { - tokenResult = confidentialClientApplication.AcquireTokenSilent(scopes, account.First()).ExecuteAsync().GetAwaiter().GetResult(); + tokenResult = confidentialClientApplication.AcquireTokenSilent(scopes, account.First()).WithForceRefresh(true).ExecuteAsync().GetAwaiter().GetResult(); } catch { - tokenResult = confidentialClientApplication.AcquireTokenForClient(scopes).ExecuteAsync().GetAwaiter().GetResult(); + try + { + tokenResult = confidentialClientApplication.AcquireTokenForClient(scopes).ExecuteAsync().GetAwaiter().GetResult(); + } + catch (MsalUiRequiredException msalEx) + { + if (msalEx.Classification == UiRequiredExceptionClassification.ConsentRequired) + { + if (clientId == PnPConnection.PnPManagementShellClientId) + { + throw new PSInvalidOperationException("Please provide consent to the PnP Management Shell application with 'Register-PnPManagementShellAccess' and follow the steps on screen."); + } + else + { + throw msalEx; + } + } + } } return new GenericToken(tokenResult.AccessToken); @@ -228,7 +247,24 @@ public static GenericToken AcquireApplicationToken(string tenant, string clientI } catch { - tokenResult = confidentialClientApplication.AcquireTokenForClient(scopes).ExecuteAsync().GetAwaiter().GetResult(); + try + { + tokenResult = confidentialClientApplication.AcquireTokenForClient(scopes).ExecuteAsync().GetAwaiter().GetResult(); + } + catch (MsalUiRequiredException msalEx) + { + if (msalEx.Classification == UiRequiredExceptionClassification.ConsentRequired) + { + if (clientId == PnPConnection.PnPManagementShellClientId) + { + throw new PSInvalidOperationException("Please provide consent to the PnP Management Shell application with 'Register-PnPManagementShellAccess' and follow the steps on screen."); + } + else + { + throw msalEx; + } + } + } } return new GenericToken(tokenResult.AccessToken); } @@ -267,7 +303,24 @@ public static GenericToken AcquireApplicationTokenInteractive(string clientId, s } catch { - tokenResult = publicClientApplication.AcquireTokenInteractive(scopes).ExecuteAsync().GetAwaiter().GetResult(); + try + { + tokenResult = publicClientApplication.AcquireTokenInteractive(scopes).WithExtraScopesToConsent(new[] { "https://graph.microsoft.com/.default" }).ExecuteAsync().GetAwaiter().GetResult(); + } + catch (MsalUiRequiredException msalEx) + { + if (msalEx.Classification == UiRequiredExceptionClassification.ConsentRequired) + { + if (clientId == PnPConnection.PnPManagementShellClientId) + { + throw new PSInvalidOperationException("Please provide consent to the PnP Management Shell application with 'Register-PnPManagementShellAccess' and follow the steps on screen."); + } + else + { + throw msalEx; + } + } + } } return new GenericToken(tokenResult.AccessToken); } @@ -351,7 +404,24 @@ public static GenericToken AcquireDelegatedTokenWithCredentials(string clientId, } catch { - tokenResult = publicClientApplication.AcquireTokenByUsernamePassword(scopes, username, securePassword).ExecuteAsync().GetAwaiter().GetResult(); + try + { + tokenResult = publicClientApplication.AcquireTokenByUsernamePassword(scopes, username, securePassword).ExecuteAsync().GetAwaiter().GetResult(); + } + catch (MsalUiRequiredException msalEx) + { + if (msalEx.Classification == UiRequiredExceptionClassification.ConsentRequired) + { + if (clientId == PnPConnection.PnPManagementShellClientId) + { + throw new PSInvalidOperationException("Please provide consent to the PnP Management Shell application with 'Register-PnPManagementShellAccess' and follow the steps on screen."); + } + else + { + throw msalEx; + } + } + } } return new GenericToken(tokenResult.AccessToken); diff --git a/Commands/Model/GraphToken.cs b/Commands/Model/GraphToken.cs index 5a9b85412..3524c8fd5 100644 --- a/Commands/Model/GraphToken.cs +++ b/Commands/Model/GraphToken.cs @@ -5,6 +5,7 @@ using PnP.PowerShell.Commands.Utilities; using System; using System.Linq; +using System.Management.Automation; using System.Security; using System.Security.Cryptography.X509Certificates; @@ -39,7 +40,7 @@ public GraphToken(string accesstoken) : base(accesstoken) public static GraphToken AcquireApplicationToken(string tenant, string clientId, X509Certificate2 certificate, AzureEnvironment azureEnvironment) { var endPoint = GenericToken.GetAzureADLoginEndPoint(azureEnvironment); - return new GraphToken(GenericToken.AcquireApplicationToken(tenant, clientId, $"{endPoint}/{tenant}", new[] { $"{ResourceIdentifier}/{DefaultScope}" }, certificate).AccessToken); + return new GraphToken(GenericToken.AcquireApplicationToken(tenant, clientId, $"{endPoint}/{tenant}", new[] { $"{ResourceIdentifier}/{DefaultScope}" }, certificate).AccessToken); } /// diff --git a/Commands/Model/SPOTenant.cs b/Commands/Model/SPOTenant.cs index 01ebc1697..3ae6b363e 100644 --- a/Commands/Model/SPOTenant.cs +++ b/Commands/Model/SPOTenant.cs @@ -37,6 +37,7 @@ public SPOTenant(Tenant tenant) this.provisionSharedWithEveryoneFolder = tenant.ProvisionSharedWithEveryoneFolder; this.signInAccelerationDomain = tenant.SignInAccelerationDomain; this.disabledWebPartIds = tenant.DisabledWebPartIds; + try { this.enableGuestSignInAcceleration = tenant.EnableGuestSignInAcceleration; @@ -471,6 +472,8 @@ public SPOTenant(Tenant tenant) public Guid[] DisabledWebPartIds => disabledWebPartIds; + public bool DisableCustomAppAuthentication => disableCustomAppAuthentication; + private bool hideDefaultThemes; private long storageQuota; @@ -600,6 +603,8 @@ public SPOTenant(Tenant tenant) private int emailAttestationReAuthDays; private Guid[] disabledWebPartIds; + + private bool disableCustomAppAuthentication; } } #endif \ No newline at end of file diff --git a/Commands/PnP.PowerShell.Commands.csproj b/Commands/PnP.PowerShell.Commands.csproj index 7143da313..df26e5ec1 100644 --- a/Commands/PnP.PowerShell.Commands.csproj +++ b/Commands/PnP.PowerShell.Commands.csproj @@ -591,6 +591,7 @@ + diff --git a/Commands/Principals/GetUser.cs b/Commands/Principals/GetUser.cs index e1518c776..342378d5d 100644 --- a/Commands/Principals/GetUser.cs +++ b/Commands/Principals/GetUser.cs @@ -47,7 +47,7 @@ namespace PnP.PowerShell.Commands.Principals Remarks = "Returns all users who have been granted explicit access to the current site, lists and listitems", SortOrder = 7)] #endif - public class GetUser : PnPWebCmdlet + public class GetUser : PnPWebRetrievalsCmdlet { private const string PARAMETERSET_IDENTITY = "Identity based request"; private const string PARAMETERSET_WITHRIGHTSASSIGNED = "With rights assigned"; @@ -79,7 +79,7 @@ public class DetailedUser protected override void ExecuteCmdlet() { - var retrievalExpressions = new Expression>[] + DefaultRetrievalExpressions = new Expression>[] { u => u.Id, u => u.Title, @@ -102,10 +102,10 @@ protected override void ExecuteCmdlet() g => g.Title, g => g.LoginName) }; - + if (Identity == null) { - SelectedWeb.Context.Load(SelectedWeb.SiteUsers, u => u.Include(retrievalExpressions)); + SelectedWeb.Context.Load(SelectedWeb.SiteUsers, u => u.Include(RetrievalExpressions)); List users = new List(); @@ -120,7 +120,7 @@ protected override void ExecuteCmdlet() var usersWithDirectPermissions = SelectedWeb.SiteUsers.Where(u => SelectedWeb.RoleAssignments.Any(ra => ra.Member.LoginName == u.LoginName)); // Get all the users contained in SharePoint Groups - SelectedWeb.Context.Load(SelectedWeb.SiteGroups, sg => sg.Include(u => u.Users.Include(retrievalExpressions), u => u.LoginName)); + SelectedWeb.Context.Load(SelectedWeb.SiteGroups, sg => sg.Include(u => u.Users.Include(RetrievalExpressions), u => u.LoginName)); SelectedWeb.Context.ExecuteQueryRetry(); // Get all SharePoint groups that have been assigned access @@ -342,7 +342,7 @@ protected override void ExecuteCmdlet() } if (user != null) { - SelectedWeb.Context.Load(user, retrievalExpressions); + SelectedWeb.Context.Load(user, RetrievalExpressions); SelectedWeb.Context.ExecuteQueryRetry(); } WriteObject(user); diff --git a/Commands/Properties/AssemblyInfo.cs b/Commands/Properties/AssemblyInfo.cs index 0f8bd973d..093ba319b 100644 --- a/Commands/Properties/AssemblyInfo.cs +++ b/Commands/Properties/AssemblyInfo.cs @@ -51,8 +51,8 @@ // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] #if !PNPPSCORE -[assembly: AssemblyVersion("3.25.2009.1")] -[assembly: AssemblyFileVersion("3.25.2009.1")] +[assembly: AssemblyVersion("3.26.2010.0")] +[assembly: AssemblyFileVersion("3.26.2010.0")] #else [assembly: AssemblyVersion("4.0.0.0")] [assembly: AssemblyFileVersion("4.0.0.0")] diff --git a/Commands/Provisioning/Site/ApplyProvisioningTemplate.cs b/Commands/Provisioning/Site/ApplyProvisioningTemplate.cs index 7ef58fac8..f488cf499 100644 --- a/Commands/Provisioning/Site/ApplyProvisioningTemplate.cs +++ b/Commands/Provisioning/Site/ApplyProvisioningTemplate.cs @@ -394,7 +394,7 @@ protected override void ExecuteCmdlet() else { // No token... - throw new PSInvalidOperationException("Your template contains artifacts that require an access token. Please provide consent to the PnP Management Shell application first by executing: Connect-PnPOnline -Graph -LaunchBrowser"); + throw new PSInvalidOperationException("Your template contains artifacts that require an access token. Please provide consent to the PnP Management Shell application first by executing: Register-PnPManagementShellAccess"); } })) { diff --git a/Commands/Provisioning/Site/ReadProvisioningTemplate.cs b/Commands/Provisioning/Site/ReadProvisioningTemplate.cs index 3c674e8af..17a6e8495 100644 --- a/Commands/Provisioning/Site/ReadProvisioningTemplate.cs +++ b/Commands/Provisioning/Site/ReadProvisioningTemplate.cs @@ -113,6 +113,9 @@ internal static ProvisioningTemplate LoadProvisioningTemplateFromFile(string tem // Load the provisioning template file Stream stream = fileConnector.GetFileStream(templateFileName); + if (stream == null) + throw new FileNotFoundException($"File {templatePath} does not exist.", templatePath); + var isOpenOfficeFile = FileUtilities.IsOpenOfficeFile(stream); XMLTemplateProvider provider; diff --git a/Commands/Provisioning/Tenant/ApplyTenantTemplate.cs b/Commands/Provisioning/Tenant/ApplyTenantTemplate.cs index 49792244d..c92573131 100644 --- a/Commands/Provisioning/Tenant/ApplyTenantTemplate.cs +++ b/Commands/Provisioning/Tenant/ApplyTenantTemplate.cs @@ -319,7 +319,7 @@ protected override void ExecuteCmdlet() } if (accessToken == null) { - throw new PSInvalidOperationException("Your template contains artifacts that require an access token. Please provide consent to the PnP Management Shell application first by executing: Connect-PnPOnline -Graph -LaunchBrowser"); + throw new PSInvalidOperationException("Your template contains artifacts that require an access token. Please provide consent to the PnP Management Shell application first by executing: Register-PnPManagementShellAccess"); } } } @@ -353,7 +353,7 @@ protected override void ExecuteCmdlet() else { // No token... - throw new PSInvalidOperationException("Your template contains artifacts that require an access token. Please provide consent to the PnP Management Shell application first by executing: Connect-PnPOnline -Graph -LaunchBrowser"); + throw new PSInvalidOperationException("Your template contains artifacts that require an access token. Please provide consent to the PnP Management Shell application first by executing: Register-PnPManagementShellAccess"); } })) { diff --git a/Commands/Provisioning/Tenant/GetTenantTemplate.cs b/Commands/Provisioning/Tenant/GetTenantTemplate.cs index 743c1fe7f..c21e3e27e 100644 --- a/Commands/Provisioning/Tenant/GetTenantTemplate.cs +++ b/Commands/Provisioning/Tenant/GetTenantTemplate.cs @@ -51,19 +51,23 @@ public class GetTenantTemplate : PnPAdminCmdlet protected override void ExecuteCmdlet() { - - ExtractConfiguration extractConfiguration = null; + ExtractConfiguration extractConfiguration; if (ParameterSpecified(nameof(Configuration))) { extractConfiguration = Configuration.GetConfiguration(SessionState.Path.CurrentFileSystemLocation.Path); - if(!string.IsNullOrEmpty(SiteUrl)) + } + else + { + extractConfiguration = new ExtractConfiguration(); + } + + if (!string.IsNullOrEmpty(SiteUrl)) + { + if (extractConfiguration.Tenant.Sequence == null) { - if(extractConfiguration.Tenant.Sequence == null) - { - extractConfiguration.Tenant.Sequence = new OfficeDevPnP.Core.Framework.Provisioning.Model.Configuration.Tenant.Sequence.ExtractSequenceConfiguration(); - } - extractConfiguration.Tenant.Sequence.SiteUrls.Add(SiteUrl); + extractConfiguration.Tenant.Sequence = new OfficeDevPnP.Core.Framework.Provisioning.Model.Configuration.Tenant.Sequence.ExtractSequenceConfiguration(); } + extractConfiguration.Tenant.Sequence.SiteUrls.Add(SiteUrl); } if (ParameterSetName == PARAMETERSET_ASFILE) diff --git a/Commands/SiteDesigns/GetSiteScript.cs b/Commands/SiteDesigns/GetSiteScript.cs index bd1c4f972..022aff5e9 100644 --- a/Commands/SiteDesigns/GetSiteScript.cs +++ b/Commands/SiteDesigns/GetSiteScript.cs @@ -35,7 +35,7 @@ protected override void ExecuteCmdlet() if (ParameterSpecified(nameof(Identity))) { var script = Tenant.GetSiteScript(ClientContext, Identity.Id); - script.EnsureProperties(s => s.Content, s => s.Title, s => s.Id, s => s.Version); + script.EnsureProperties(s => s.Content, s => s.Title, s => s.Id, s => s.Version, s => s.Description); WriteObject(script); } else if (ParameterSpecified(nameof(SiteDesign))) @@ -54,7 +54,7 @@ protected override void ExecuteCmdlet() else { var scripts = Tenant.GetSiteScripts(); - ClientContext.Load(scripts, s => s.Include(sc => sc.Id, sc => sc.Title, sc => sc.Version, sc => sc.Content)); + ClientContext.Load(scripts, s => s.Include(sc => sc.Id, sc => sc.Title, sc => sc.Version, sc => sc.Content, sc => sc.Description)); ClientContext.ExecuteQueryRetry(); WriteObject(scripts.ToList(), true); } diff --git a/Commands/Teams/GetTeamsChannelMessage.cs b/Commands/Teams/GetTeamsChannelMessage.cs index e43124dce..fc88eb3ae 100644 --- a/Commands/Teams/GetTeamsChannelMessage.cs +++ b/Commands/Teams/GetTeamsChannelMessage.cs @@ -13,8 +13,8 @@ namespace PnP.PowerShell.Commands.Graph Category = CmdletHelpCategory.Teams, SupportedPlatform = CmdletSupportedPlatform.Online)] [CmdletExample( - Code = "PS:> Submit-PnPTeamsChannelMessage -Team MyTestTeam -Channel \"My Channel\" -Message \"A new message\"", - Remarks = "Sends \"A new message\" to the specified channel", + Code = "PS:> Get-PnPTeamsChannelMessage -Team 07d9dbfb-cbf2-4dc1-92c6-7921f5931b3d -Channel \"My Channel\"", + Remarks = "Gets all messages of the specified channel", SortOrder = 1)] [CmdletMicrosoftGraphApiPermission(MicrosoftGraphApiPermission.Group_ReadWrite_All)] public class GetTeamsChannelMessage : PnPGraphCmdlet diff --git a/Commands/Teams/NewTeamsTeam.cs b/Commands/Teams/NewTeamsTeam.cs index 77c3b1092..aefdde7c9 100644 --- a/Commands/Teams/NewTeamsTeam.cs +++ b/Commands/Teams/NewTeamsTeam.cs @@ -14,16 +14,12 @@ namespace PnP.PowerShell.Commands.Graph Category = CmdletHelpCategory.Teams, SupportedPlatform = CmdletSupportedPlatform.Online)] [CmdletExample( - Code = "PS:> Get-PnPTeamsTeam", - Remarks = "Retrieves all the Microsoft Teams instances", + Code = @"PS:> New-PnPTeamsTeam -DisplayName ""myPnPDemo1"" -Visibility Private -AllowCreateUpdateRemoveTabs $false -AllowUserDeleteMessages $false", + Remarks = @"This will create a new Team called ""myPnPDemo1"" and sets the privacy to Private, as well as preventing users from deleting their messages or update/remove tabs. The user creating the Team will be added as Owner.", SortOrder = 1)] [CmdletExample( - Code = "PS:> Get-PnPTeamsTeam -GroupId $groupId", - Remarks = "Retrieves a specific Microsoft Teams instance", - SortOrder = 2)] - [CmdletExample( - Code = "PS:> Get-PnPTeamsTeam -Visibility Public", - Remarks = "Retrieves all Microsoft Teams instances which are public visible", + Code = "PS:> New-PnPTeamsTeam -GroupId $groupId", + Remarks = "This will create a new Team from a Microsoft 365 Group using the Group ID", SortOrder = 2)] [CmdletMicrosoftGraphApiPermission(MicrosoftGraphApiPermission.Group_ReadWrite_All)] public class NewTeamsTeam : PnPGraphCmdlet @@ -137,4 +133,4 @@ protected override void ExecuteCmdlet() } } } -#endif \ No newline at end of file +#endif diff --git a/Commands/Utilities/CertificateHelper.cs b/Commands/Utilities/CertificateHelper.cs index 0d652fe82..9899e62b9 100644 --- a/Commands/Utilities/CertificateHelper.cs +++ b/Commands/Utilities/CertificateHelper.cs @@ -142,16 +142,30 @@ internal static X509Certificate2 GetCertificateFromPEMstring(string publicCert, internal static X509Certificate2 GetCertificateFromPath(string certificatePath, SecureString certificatePassword) { - var certFile = System.IO.File.OpenRead(certificatePath); - var certificateBytes = new byte[certFile.Length]; - certFile.Read(certificateBytes, 0, (int)certFile.Length); - var certificate = new X509Certificate2( - certificateBytes, - certificatePassword, - X509KeyStorageFlags.Exportable | - X509KeyStorageFlags.MachineKeySet | - X509KeyStorageFlags.PersistKeySet); - return certificate; + if (System.IO.File.Exists(certificatePath)) + { + var certFile = System.IO.File.OpenRead(certificatePath); + if (certFile.Length == 0) + throw new Exception($"The specified certificate path '{certificatePath}' points to an empty file"); + + var certificateBytes = new byte[certFile.Length]; + certFile.Read(certificateBytes, 0, (int)certFile.Length); + var certificate = new X509Certificate2( + certificateBytes, + certificatePassword, + X509KeyStorageFlags.Exportable | + X509KeyStorageFlags.MachineKeySet | + X509KeyStorageFlags.PersistKeySet); + return certificate; + } + else if (System.IO.Directory.Exists(certificatePath)) + { + throw new FileNotFoundException($"The specified certificate path '{certificatePath}' points to a folder", certificatePath); + } + else + { + throw new FileNotFoundException($"The specified certificate path '{certificatePath}' does not exist", certificatePath); + } } diff --git a/Commands/Web/SetRequestAccessEmails.cs b/Commands/Web/SetRequestAccessEmails.cs index 0e3b482d7..c479f9530 100644 --- a/Commands/Web/SetRequestAccessEmails.cs +++ b/Commands/Web/SetRequestAccessEmails.cs @@ -64,10 +64,7 @@ protected override void ExecuteCmdlet() else { // Enable requesting access and set it to the default owners group - // Code can be replaced by SelectedWeb.EnableRequestAccess(); once https://github.com/SharePoint/PnP-Sites-Core/pull/2533 has been accepted for merge. - SelectedWeb.SetUseAccessRequestDefaultAndUpdate(true); - SelectedWeb.Update(); - SelectedWeb.Context.ExecuteQueryRetry(); + SelectedWeb.EnableRequestAccess(); } } } diff --git a/PnP_PowerShell_Roadmap.png b/PnP_PowerShell_Roadmap.png new file mode 100644 index 000000000..cfbff7ab3 Binary files /dev/null and b/PnP_PowerShell_Roadmap.png differ diff --git a/README.md b/README.md index b0bac5ef0..99a1c7ddd 100644 --- a/README.md +++ b/README.md @@ -5,31 +5,19 @@ This solution contains a library of PowerShell commands that allows you to perfo ![SharePoint Patterns and Practices](https://devofficecdn.azureedge.net/media/Default/PnP/sppnp.png) -### Applies to ### -- Sharepoint Online (Multi Tenant & Dedicated) -- SharePoint 2019 on-premises -- SharePoint 2016 on-premises -- SharePoint 2013 on-premises +> **Important:** +> This repository will be retired by the end of 2020. As of the GA of the Cross-Platform [PnP.PowerShell](https://github.com/pnp/pnp.powershell) we will only maintain that version going forward. -### Prerequisites ### -In order to generate the Cmdlet help you need to have the Windows Management Framework v4.0 installed, which you can download from http://www.microsoft.com/en-us/download/details.aspx?id=40855 +![PnP PowerShell RoadMap](PnP_PowerShell_Roadmap.png) -If it is not [pre-installed on your operating system](https://docs.microsoft.com/powershell/scripting/wmf/overview#wmf-availability-across-windows-operating-systems), you can find installation instructions in the [WMF release notes.](https://docs.microsoft.com/powershell/scripting/wmf/overview#wmf-release-notes) -Check out the "Getting Started" section to make sure you have all requirements in place. +# I've found a bug, where do I need to log an issue or create a PR -### Latest Release Quick Download +Between now and the end of 2020 both [PnP-PowerShell](https://github.com/pnp/pnp-powershell) and [PnP.PowerShell](https://github.com/pnp/powershell) are actively maintained. Once the new PnP PowerShell becomes generally available (GA) we will stop mainting the old repository. -The latest release can be found on [this link](https://github.com/pnp/PnP-PowerShell/releases) +Given that the cross-platform PnP PowerShell is our future going forward we would prefer issues and PRs being created in the new https://github.com/pnp/powershell repository. If you want your PR to apply to both then it is recommend to create the PR in both repositories for the time being. ----------- -# Commands included # -[Navigate here for an overview of all cmdlets and their parameters](https://docs.microsoft.com/powershell/sharepoint/sharepoint-pnp/sharepoint-pnp-cmdlets?view=sharepoint-ps) - -# Installation # -There are two ways: - -## 1. Using the [PowerShell Gallery](https://www.powershellgallery.com) **(Recommended)** +# Installation Using the [PowerShell Gallery](https://www.powershellgallery.com) If you main OS is Windows 10, or if you have [PowerShellGet](https://github.com/powershell/powershellget) installed, you can run the following commands to install the PowerShell cmdlets: @@ -40,16 +28,8 @@ If you main OS is Windows 10, or if you have [PowerShellGet](https://github.com/ |SharePoint 2016|```Install-Module SharePointPnPPowerShell2016```| |SharePoint 2013|```Install-Module SharePointPnPPowerShell2013```| -*Notice*: if you install the latest PowerShellGet from Github, you might receive an error message stating ->PackageManagement\Install-Package : The version 'x.x.x.x' of the module 'SharePointPnPPowerShellOnline' being installed is not catalog signed. - -In order to install the cmdlets when you get this error specify the -SkipPublisherCheck switch with the Install-Module cmdlet, e.g. ```Install-Module SharePointPnPPowerShellOnline -SkipPublisherCheck -AllowClobber``` - -## 2. Downloading the Files directly - -You can download the setup files from the [releases](https://github.com/pnp/PnP-PowerShell/releases) section of the PnP PowerShell repository. These files will up be updated on a monthly basis. Run the install and restart any open instances of PowerShell to use the cmdlets. -### How to Update the Cmdlets +# How to Update the Cmdlets Every month a new release will be made available of the PnP PowerShell Cmdlets. If you earlier installed the cmdlets using the setup file, simply download the [latest version](https://github.com/pnp/PnP-PowerShell/releases/latest) and run the setup. This will update your existing installation. If you have installed the cmdlets using PowerShellGet with ```Install-Module``` from the PowerShell Gallery then you will be able to use the following command to install the latest updated version: diff --git a/Samples/Connect.AzureAutomation/Deploy-AzureAppOnly.ps1 b/Samples/Connect.AzureAutomation/Deploy-AzureAppOnly.ps1 new file mode 100644 index 000000000..ef82ee69a --- /dev/null +++ b/Samples/Connect.AzureAutomation/Deploy-AzureAppOnly.ps1 @@ -0,0 +1,96 @@ +<# +---------------------------------------------------------------------------- + +Deploys resources to Azure Automation, Installs PnP PowerShell, Creates an Azure AD App + +Created: Paul Bullock +Date: 10/08/2020 +Disclaimer: + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +.Synopsis + +.Example + +.Notes + + Default App Scopes: Sites.FullControl.All, Group.ReadWrite.All, User.Read.All + + References: + https://docs.microsoft.com/en-us/powershell/module/sharepoint-pnp/initialize-pnppowershellauthentication?view=sharepoint-ps + https://docs.microsoft.com/en-us/powershell/module/az.automation/New-AzAutomationCredential?view=azps-4.4.0 + + ---------------------------------------------------------------------------- +#> + +[CmdletBinding()] +param ( + [Parameter(Mandatory = $true)] + [string] $Tenant, #yourtenant.onmicrosoft.com + + [Parameter(Mandatory = $true)] + [string] $SPTenant, # https://[thispart].sharepoint.com + + [Parameter(Mandatory = $false)] + [string] $AppName = "PnP-PowerShell Automation", + + [Parameter(Mandatory = $true)] + [string] $CertificatePassword, # <-- Use a nice a super complex password + + [Parameter(Mandatory = $false)] + [int] $ValidForYears = 2, + + [Parameter(Mandatory = $false)] + [string] $CertCommonName = "PnP-PowerShell Automation" +) +begin{ + + + Write-Host "Let's get started..." + + # Get the location of the script to copy the script locally + $location = Get-Location + + if(!$CertificatePassword){ + Write-Host " - Password generated for you..." + $CertificatePassword = [System.Guid]::NewGuid() + } + + if(!$CertCommonName){ + $CertCommonName = "pnp.$($Tenant)" + } + + # This cna be a one-time setup - no one needs to know the password, it can be easily replaced + # in the App and Automation Service if required + $securePassword = (ConvertTo-SecureString -String $CertificatePassword -AsPlainText -Force) + +} +process { + + # ---------------------------------------------------------------------------------- + # Azure - Create Azure App and Certificate + # ---------------------------------------------------------------------------------- + Write-Host " - Registering AD app and creating certificate..." -ForegroundColor Cyan + + Initialize-PnPPowerShellAuthentication -ApplicationName $AppName -Tenant $Tenant -OutPath $location ` + -CertificatePassword $securePassword -ValidYears $ValidForYears ` + -CommonName $CertCommonName + + # Example Output: + # Pfx file : C:\Git\tfs\Script-Library\Azure\Automation\Deploy\PnP-PowerShell Automation.pfx + # Cer file : C:\Git\tfs\Script-Library\Azure\Automation\Deploy\PnP-PowerShell Automation.cer + # AzureAppId : c5beca65-0000-1111-2222-8a02cbbf4c4d + # Certificate Thumbprint : 78D0F76D900000C8B9F77E64903B6D7AEF55D233 + +} +end{ + + Write-Host "Script all done, enjoy! :)" -ForegroundColor Green +} \ No newline at end of file diff --git a/Samples/Connect.AzureAutomation/Deploy-AzureAutomation.ps1 b/Samples/Connect.AzureAutomation/Deploy-AzureAutomation.ps1 new file mode 100644 index 000000000..0cb3c2f9d --- /dev/null +++ b/Samples/Connect.AzureAutomation/Deploy-AzureAutomation.ps1 @@ -0,0 +1,191 @@ +<# +---------------------------------------------------------------------------- + +Deploys resources to Azure Automation, Installs PnP PowerShell, Creates an Azure AD App + +Created: Paul Bullock +Date: 10/08/2020 +Disclaimer: + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +.Notes + + Default App Scopes: Sites.FullControl.All, Group.ReadWrite.All, User.Read.All + + References: + https://docs.microsoft.com/en-us/powershell/module/sharepoint-pnp/initialize-pnppowershellauthentication?view=sharepoint-ps + https://docs.microsoft.com/en-us/powershell/module/az.automation/New-AzAutomationCredential?view=azps-4.4.0 + + Due credit to sources, some learnings in the script came from: + https://github.com/OfficeDev/microsoft-teams-apps-requestateam + + ---------------------------------------------------------------------------- +#> + +[CmdletBinding()] +param ( + [Parameter(Mandatory = $true)] + [string] $Tenant, #yourtenant.onmicrosoft.com + + [Parameter(Mandatory = $true)] + [string] $SPTenant, # https://[thispart].sharepoint.com + + [Parameter(Mandatory = $true)] + [string] $CertificatePassword, # <-- Use a nice a super complex password + + [Parameter(Mandatory = $true)] + [string] $AzureAppId, + + [Parameter(Mandatory = $true)] + [string] $CertificatePath, # e.g. "C:\Git\tfs\Script-Library\Azure\Automation\Deploy\PnP-PowerShell Automation.pfx" + + [Parameter(Mandatory = $false)] + [string] $AzureResourceGroupName = "pnp-powershell-automation-rg", + + [Parameter(Mandatory = $false)] + [string] $AzureRegion = "northeurope", + + [Parameter(Mandatory = $false)] + [string] $AzureAutomationName = "pnp-powershell-auto", + + [Parameter(Mandatory = $false)] + [switch] $CreateResourceGroup +) +begin{ + + + Write-Host "Let's get started..." + + # This cna be a one-time setup - no one needs to know the password, it can be easily replaced + # in the App and Automation Service if required + $securePassword = (ConvertTo-SecureString -String $CertificatePassword -AsPlainText -Force) + +} +process { + + # ---------------------------------------------------------------------------------- + # Azure - Connect to Azure + # ---------------------------------------------------------------------------------- + Write-Host " - Connecting to Azure..." -ForegroundColor Cyan + Connect-AzAccount + + # ---------------------------------------------------------------------------------- + # Azure - Resource Group + # ---------------------------------------------------------------------------------- + + # Check if the Resource Group exists + if($CreateResourceGroup){ + Write-Host " - Creating Resource Group..." -ForegroundColor Cyan + New-AzResourceGroup -Name $AzureResourceGroupName -Location $AzureRegion + } + + + # ---------------------------------------------------------------------------------- + # Azure Automation - Creation + # ---------------------------------------------------------------------------------- + + # Validate this does not already exist + $existingAzAutomation = Get-AzAutomationAccount | Where-Object AutomationAccountName -eq $AzureAutomationName + if ($null -ne $existingAzAutomation) { + Write-Error " - Automation account already exists...aborting deployment script" # Stop the script, already exists + return #End the Script + } + + Write-Host " - Creating Azure Automation Account..." -ForegroundColor Cyan + + # Note: Not all regions support Azure Automation - check here for your region: + # https://azure.microsoft.com/en-us/global-infrastructure/services/?products=automation®ions=all + New-AzAutomationAccount ` + -Name $AzureAutomationName ` + -Location $AzureRegion ` + -ResourceGroupName $AzureResourceGroupName + + # ---------------------------------------------------------------------------------- + # Azure Automation - Add Modules + # ---------------------------------------------------------------------------------- + + # Add PnP Modules - July 2020 Onwards + New-AzAutomationModule ` + -AutomationAccountName $AzureAutomationName ` + -Name "SharePointPnPPowerShellOnline" ` + -ContentLink "https://devopsgallerystorage.blob.core.windows.net/packages/sharepointpnppowershellonline.3.23.2007.1.nupkg" ` + -ResourceGroupName $AzureResourceGroupName + + + # ---------------------------------------------------------------------------------- + # Azure Automation - Create variables + # ---------------------------------------------------------------------------------- + New-AzAutomationVariable ` + -AutomationAccountName $AzureAutomationName ` + -Name "AppClientId" ` + -Encrypted $False ` + -Value $AzureAppId ` + -ResourceGroupName $AzureResourceGroupName + + New-AzAutomationVariable ` + -AutomationAccountName $AzureAutomationName ` + -Name "AppAdTenant" ` + -Encrypted $true ` + -Value $Tenant ` + -ResourceGroupName $AzureResourceGroupName + + New-AzAutomationVariable ` + -AutomationAccountName $AzureAutomationName ` + -Name "App365Tenant" ` + -Encrypted $true ` + -Value $SPTenant ` + -ResourceGroupName $AzureResourceGroupName + + New-AzAutomationCertificate ` + -Name "AzureAppCertificate" ` + -Description "Certificate for PnP PowerShell automation" ` + -Password $securePassword ` + -Path $CertificatePath ` + -Exportable ` + -ResourceGroupName $AzureResourceGroupName ` + -AutomationAccountName $AzureAutomationName + + # In this example, we do not use the UserName part + $User = "IAamNotUsed" + $Credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $User, $securePassword + New-AzAutomationCredential ` + -Name "AzureAppCertPassword" ` + -Description "Contains the password for the certificate" ` + -Value $Credential ` + -ResourceGroupName $AzureResourceGroupName ` + -AutomationAccountName $AzureAutomationName ` + + # Add Azure Runbook + Write-Host " - Importing and publishing example runbook..." -ForegroundColor Cyan + + # Import automation runbooks + $exampleRunbookName = "test-connection-runbook" + + # Add the example runbook into Azure Automation + Import-AzAutomationRunbook ` + -Name $exampleRunbookName ` + -Path "./$($exampleRunbookName).ps1" ` + -ResourceGroupName $AzureResourceGroupName ` + -AutomationAccountName $AzureAutomationName ` + -Type PowerShell + + # Publish runbooks + Publish-AzAutomationRunbook ` + -Name $exampleRunbookName ` + -ResourceGroupName $AzureResourceGroupName ` + -AutomationAccountName $AzureAutomationName + + Write-Host "Finished adding example runbook" -ForegroundColor Green + +} +end{ + + Write-Host "Script all done, enjoy! :)" -ForegroundColor Green +} \ No newline at end of file diff --git a/Samples/Connect.AzureAutomation/Deploy-FullAutomation.ps1 b/Samples/Connect.AzureAutomation/Deploy-FullAutomation.ps1 new file mode 100644 index 000000000..87149fb9c --- /dev/null +++ b/Samples/Connect.AzureAutomation/Deploy-FullAutomation.ps1 @@ -0,0 +1,225 @@ +<# +---------------------------------------------------------------------------- + +Deploys resources to Azure Automation, Installs PnP PowerShell, Creates an Azure AD App + +Created: Paul Bullock +Date: 10/08/2020 +Disclaimer: + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +.Notes + + Default App Scopes: Sites.FullControl.All, Group.ReadWrite.All, User.Read.All + + References: + https://docs.microsoft.com/en-us/powershell/module/sharepoint-pnp/initialize-pnppowershellauthentication?view=sharepoint-ps + https://docs.microsoft.com/en-us/powershell/module/az.automation/New-AzAutomationCredential?view=azps-4.4.0 + + Due credit to sources, some learnings in the script came from: + https://github.com/OfficeDev/microsoft-teams-apps-requestateam + + ---------------------------------------------------------------------------- +#> + + + +[CmdletBinding()] +param ( + [Parameter(Mandatory = $true)] + [string] $Tenant, #yourtenant.onmicrosoft.com + + [Parameter(Mandatory = $true)] + [string] $SPTenant, # https://[thispart].sharepoint.com + + [Parameter(Mandatory = $false)] + [string] $AppName = "PnP-PowerShell Automation", + + [Parameter(Mandatory = $true)] + [string] $CertificatePassword, # <-- Use a nice a super complex password + + [Parameter(Mandatory = $false)] + [int] $ValidForYears = 2, + + [Parameter(Mandatory = $false)] + [string] $CertCommonName = "PnP-PowerShell Automation", + + [Parameter(Mandatory = $false)] + [string] $AzureResourceGroupName = "pnp-powershell-automation-rg", + + [Parameter(Mandatory = $false)] + [string] $AzureRegion = "northeurope", + + [Parameter(Mandatory = $false)] + [string] $AzureAutomationName = "pnp-powershell-auto", + + [Parameter(Mandatory = $false)] + [switch] $CreateResourceGroup +) +begin{ + + + Write-Host "Let's get started..." + + # Get the location of the script to copy the script locally + $location = Get-Location + + if(!$CertificatePassword){ + Write-Host " - Password generated for you..." + $CertificatePassword = [System.Guid]::NewGuid() + } + + if(!$CertCommonName){ + $CertCommonName = "pnp.$($Tenant)" + } + + # This cna be a one-time setup - no one needs to know the password, it can be easily replaced + # in the App and Automation Service if required + $securePassword = (ConvertTo-SecureString -String $CertificatePassword -AsPlainText -Force) + +} +process { + + # ---------------------------------------------------------------------------------- + # Azure - Create Azure App and Certificate + # ---------------------------------------------------------------------------------- + Write-Host " - Registering AD app and creating certificate..." -ForegroundColor Cyan + + $result = Initialize-PnPPowerShellAuthentication -ApplicationName $AppName -Tenant $Tenant -OutPath $location ` + -CertificatePassword $securePassword -ValidYears $ValidForYears ` + -CommonName $CertCommonName + + + # Pfx file : C:\Git\tfs\Script-Library\Azure\Automation\Deploy\PnP-PowerShell Automation.pfx + # Cer file : C:\Git\tfs\Script-Library\Azure\Automation\Deploy\PnP-PowerShell Automation.cer + # AzureAppId : c5beca65-bb78-414b-bd95-8a02cbbf4c4d + # Certificate Thumbprint : 78D0F76D907FB9C8B9F77E64903B6D7AEF55D233 + + $generatedAppId = $result.AzureAppId + $generatedPfxCertPath = "$($location)\$($CertCommonName).pfx" + + # ---------------------------------------------------------------------------------- + # Azure - Connect to Azure + # ---------------------------------------------------------------------------------- + Write-Host " - Connecting to Azure..." -ForegroundColor Cyan + Connect-AzAccount + + # ---------------------------------------------------------------------------------- + # Azure - Resource Group + # ---------------------------------------------------------------------------------- + + # Check if the Resource Group exists + if($CreateResourceGroup){ + Write-Host " - Creating Resource Group..." -ForegroundColor Cyan + New-AzResourceGroup -Name $AzureResourceGroupName -Location $AzureRegion + } + + # ---------------------------------------------------------------------------------- + # Azure Automation - Creation + # ---------------------------------------------------------------------------------- + + # Validate this does not already exist + $existingAzAutomation = Get-AzAutomationAccount | Where-Object AutomationAccountName -eq $AppName + if ($null -ne $existingAzAutomation) { + Write-Error " - Automation account already exists...aborting deployment script" # Stop the script, already exists + return #End the Script + } + + Write-Host " - Creating Azure Automation Account..." -ForegroundColor Cyan + + # Note: Not all regions support Azure Automation - check here for your region: + # https://azure.microsoft.com/en-us/global-infrastructure/services/?products=automation®ions=all + New-AzAutomationAccount ` + -Name $AzureAutomationName ` + -Location $AzureRegion ` + -ResourceGroupName $AzureResourceGroupName + + # ---------------------------------------------------------------------------------- + # Azure Automation - Add Modules + # ---------------------------------------------------------------------------------- + + # Add PnP Modules - July 2020 Onwards + New-AzAutomationModule ` + -AutomationAccountName $AzureAutomationName ` + -Name "SharePointPnPPowerShellOnline" ` + -ContentLink "https://devopsgallerystorage.blob.core.windows.net/packages/sharepointpnppowershellonline.3.23.2007.1.nupkg" ` + -ResourceGroupName $AzureResourceGroupName + + + # ---------------------------------------------------------------------------------- + # Azure Automation - Create variables + # ---------------------------------------------------------------------------------- + New-AzAutomationVariable ` + -AutomationAccountName $AzureAutomationName ` + -Name "AppClientId" ` + -Encrypted $False ` + -Value $generatedAppId ` + -ResourceGroupName $AzureResourceGroupName + + New-AzAutomationVariable ` + -AutomationAccountName $AzureAutomationName ` + -Name "AppAdTenant" ` + -Encrypted $true ` + -Value $Tenant ` + -ResourceGroupName $AzureResourceGroupName + + New-AzAutomationVariable ` + -AutomationAccountName $AzureAutomationName ` + -Name "App365Tenant" ` + -Encrypted $true ` + -Value $SPTenant ` + -ResourceGroupName $AzureResourceGroupName + + New-AzAutomationCertificate ` + -Name "AzureAppCertificate" ` + -Description "Certificate for PnP PowerShell automation" ` + -Password $securePassword ` + -Path $generatedPfxCertPath ` + -Exportable ` + -ResourceGroupName $AzureResourceGroupName ` + -AutomationAccountName $AzureAutomationName + + # In this example, we do not use the UserName part + $User = "IAamNotUsed" + $Credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $User, $securePassword + New-AzAutomationCredential ` + -Name "AzureAppCertPassword" ` + -Description "Contains the password for the certificate" ` + -Value $Credential ` + -ResourceGroupName $AzureResourceGroupName ` + -AutomationAccountName $AzureAutomationName ` + + # Add Azure Runbook + Write-Host " - Importing and publishing example runbook..." -ForegroundColor Cyan + + # Import automation runbooks + $exampleRunbookName = "test-connection-runbook" + + # Add the example runbook into Azure Automation + Import-AzAutomationRunbook ` + -Name $exampleRunbookName ` + -Path "./$($exampleRunbookName).ps1" ` + -ResourceGroupName $AzureResourceGroupName ` + -AutomationAccountName $AzureAutomationName ` + -Type PowerShell + + # Publish runbooks + Publish-AzAutomationRunbook ` + -Name $exampleRunbookName ` + -ResourceGroupName $AzureResourceGroupName ` + -AutomationAccountName $AzureAutomationName + + Write-Host "Finished adding example runbook" -ForegroundColor Green + +} +end{ + + Write-Host "Script all done, enjoy! :)" -ForegroundColor Green +} \ No newline at end of file diff --git a/Samples/Connect.AzureAutomation/Readme.md b/Samples/Connect.AzureAutomation/Readme.md new file mode 100644 index 000000000..34534d970 --- /dev/null +++ b/Samples/Connect.AzureAutomation/Readme.md @@ -0,0 +1,93 @@ +# Connect to the SharePoint Online using Application Permissions + +This PowerShell sample demonstrates how to deploy and use the PnP PowerShell to connect to SharePoint Online +using App-Only within Azure Automation. This is useful for demonstrating connecting to SharePoint Online with App-Only permissions +as well as provisioning Azure Automation with the required modules. + +Applies to + +- Office 365 Multi-Tenant (MT) + +## Prerequisites + +- PnP PowerShell Module (Minimum 3.23.2007.1) +- Azure AD - Global Admin (for app consent) +- Azure PowerShell Module [https://docs.microsoft.com/en-us/powershell/azure/install-az-ps?view=azps-4.4.0](https://docs.microsoft.com/en-us/powershell/azure/install-az-ps?view=azps-4.4.0) + +## Scripts + +The following script samples as part of the solution: + +- Deploy-AzureAppOnly.ps1 - this uses the new cmdlet "Initialize-PnPPowerShellAuthentication" to create the certificate and create Azure AD app +- Deploy-AzureAutomation.ps1 - this creates an Azure Automation account, configures the account for hosting credentials, registering the PnP module and publishing a runbook +- Deploy-FullAutomation.ps1 - this is a combination of both above scripts in one run +- test-connection-runbook.ps1 - sample runbook that connects to SharePoint Online with a certificate and example connections to tenant level and site level + +### Note + +Not all regions support Azure Automation - check here for your region: [https://azure.microsoft.com/en-us/global-infrastructure/services/?products=automation®ions=all](https://azure.microsoft.com/en-us/global-infrastructure/services/?products=automation®ions=all) + +## Getting Started + +### Example 1: Creation of an Azure AD App + +```powershell + +./Deploy-AzureAppOnly.ps1 ` + -Tenant "yourtenant.onmicrosoft.com" ` + -SPTenant "yourtenant" ` + -AppName "PnP-PowerShell Automation" ` + -CertificatePassword "" ` + -ValidForYears 2 ` + -CertCommonName "PnP-PowerShell Automation" + +Note: It is recommended to use a better Certificate Password than above, nice and super complex + +``` + +### Example 2: Provisioning Azure Automation with PnP Module, credentials and publishing runbook + +```powershell + +./Deploy-AzureAutomation.ps1 ` + -Tenant "yourtenant.onmicrosoft.com" ` + -SPTenant "yourtenant" ` + -CertificatePassword "" ` + -AzureAppId "b80b83e9-2d52-4aa4-910a-099c296b36d4" ` + -CertificatePath "C:\Git\tfs\Script-Library\Azure\Automation\Deploy\PnP-PowerShell Automation.pfx" ` + -AzureResourceGroupName "pnp-powershell-automation-rg" ` + -AzureRegion "northeurope" ` + -AzureAutomationName "pnp-powershell-auto" ` + -CreateResourceGroup + +``` + +Notes: +- Use a better Certificate Password than above but the same as the one in example 1, nice and super complex +- The CertificatePath is the location where the certificate was stored locally in example 1 + +### Example 3: Combination Script of both Example 1 and 2 + +```powershell + +./Deploy-FullAutomation.ps1 ` + -Tenant "yourtenant.onmicrosoft.com" ` + -SPTenant "yourtenant" ` + -AppName "PnP-PowerShell Automation" ` + -CertificatePassword "" ` + -ValidForYears 2 ` + -CertCommonName "PnP-PowerShell Automation" ` + -AzureResourceGroupName "pnp-powershell-automation-rg" ` + -AzureRegion "northeurope" ` + -AzureAutomationName "pnp-powershell-auto" ` + -CreateResourceGroup +``` + +## Version history ## +Version | Date | Author(s) | Comments +---------| ---- | --------- | ---------| +1.0 | August 10th 2020 | Paul Bullock (CaPa Creative Ltd) | Initial release + + +## **Disclaimer** +THIS CODE IS PROVIDED AS IS WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT. \ No newline at end of file diff --git a/Samples/Connect.AzureAutomation/test-connection-runbook.ps1 b/Samples/Connect.AzureAutomation/test-connection-runbook.ps1 new file mode 100644 index 000000000..bab028b08 --- /dev/null +++ b/Samples/Connect.AzureAutomation/test-connection-runbook.ps1 @@ -0,0 +1,68 @@ +<# ---------------------------------------------------------------------------- + +Example script connecting to SharePoint Online with a + App Only Certificate in Azure Automation + +Created: Paul Bullock +Date: 10/08/2020 +Disclaimer: + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +---------------------------------------------------------------------------- #> + +[CmdletBinding()] +Param +() + +# Retrieves from the Azure Automation variables and certificate stores +# the details for connecting to SharePoint Online +$azureAutomateCreds = Get-AutomationPSCredential -Name 'AzureAppCertPassword' +$appId = Get-AutomationVariable -Name "AppClientId" +$appAdTenant = Get-AutomationVariable -Name "AppAdTenant" +$app365Tenant = Get-AutomationVariable -Name "App365Tenant" +$appCert = Get-AutomationCertificate -Name "AzureAppCertificate" + +# Addresses for the tenant +$adminUrl = "https://$app365Tenant-admin.sharepoint.com" +$baseSite = "https://$app365Tenant.sharepoint.com" + +# Site Template List + +try { + Write-Verbose "Running Script..." + + # Export the certificate and convert into base 64 string + $base64Cert = [System.Convert]::ToBase64String($appCert.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Pkcs12)) + + # Connect to the standard SharePoint Site + $siteConn = Connect-PnPOnline -ClientId $appId -CertificateBase64Encoded $base64Cert ` + -CertificatePassword $azureAutomateCreds.Password ` + -Url $baseSite -Tenant $appAdTenant -ReturnConnection + + # Connect to the SharePoint Online Admin Service + $adminSiteConn = Connect-PnPOnline -ClientId $appId -CertificateBase64Encoded $base64Cert ` + -CertificatePassword $azureAutomateCreds.Password ` + -Url $adminUrl -Tenant $appAdTenant -ReturnConnection + + # SharePointy Stuff here + Write-Verbose "Connected to SharePoint Online Site" + $web = Get-PnPWeb -Connection $siteConn + $web.Title + + # SharePointy Adminy Stuff here + Write-Verbose "Connected to SharePoint Online Admin Centre" + $tenantSite = Get-PnPTenantSite -Url $baseSite -Connection $adminSiteConn + $tenantSite.Title + +} +catch { + #Script error + Write-Error "An error occurred: $($PSItem.ToString())" +} \ No newline at end of file diff --git a/version.txt b/version.txt index 1bda19773..639fcf7a9 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -3.25.2009.1 \ No newline at end of file +3.26.2010.0 \ No newline at end of file