diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d80f945d..492ad70a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). ### Added -- Added `Add\Remove\Invoke-PnPListDesign` cmdlets to add a list design, remove a list design and apply the list design. +- Added `Add\Remove\Invoke-PnPListDesign` cmdlets to add a list design, remove a list design and apply the list design +- 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` ### Changed @@ -74,6 +75,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). - Added `-IsFluidEnabled` to `Set-PnPTenant` cmdlet to enable/disable users from using Fluid components. - Added `Add\Get\Remove-PnPListItemComment` cmdlets to deal with list item comments. Using these cmdlets, you will now be able to add, retrieve and delete list item comments. [#1462](https://github.com/pnp/powershell/pull/1462) - Added `-ResourceTypeName` and `-ResourceUrl` parameters to `Get-PnPAccessToken` to fetch access token of specified resource. [#1451](https://github.com/pnp/powershell/pull/1451) +- Added `-BookmarkStatus` parameter to `Get-PnPSearchConfiguration` cmdlet to call REST endpoint to fetch promoted results defined via query rules and output them in Bookmark supported CSV format. ### Changed - Improved `Get-PnPFile` cmdlet to handle large file downloads @@ -111,6 +113,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). - Removed `Get-PnPSubWebs` as that was marked deprecated a year ago. Use `Get-PnPSubWeb` instead. [#1394](https://github.com/pnp/powershell/pull/1394) ### Contributors +- Mikael Svenson [wobba] - Koen Zomers [koenzomers] - Bert Jansen [jansenbe] - Gautam Sheth [gautamdsheth] @@ -673,4 +676,4 @@ First released version of PnP PowerShell - Koen Zomers [koenzomers] - Carlos Marins Jr [kadu-jr] - Aimery Thomas [a1mery] -- Veronique Lengelle [veronicageek] +- Veronique Lengelle [veronicageek] \ No newline at end of file diff --git a/documentation/New-PnPTeamsTeam.md b/documentation/New-PnPTeamsTeam.md index fe6b71761..3a11c2bdc 100644 --- a/documentation/New-PnPTeamsTeam.md +++ b/documentation/New-PnPTeamsTeam.md @@ -29,7 +29,7 @@ New-PnPTeamsTeam -GroupId [-Owner ] [-AllowAddRemoveApps ] [-AllowStickersAndMemes ] [-AllowTeamMentions ] [-AllowUserDeleteMessages ] [-AllowUserEditMessages ] [-GiphyContentRating ] [-ShowInTeamsSearchAndSuggestions ] - [-Classification ] [] + [-Classification ] [-Owners ] [-Members ] [] ``` ### For a new group @@ -42,7 +42,8 @@ New-PnPTeamsTeam -DisplayName [-MailNickName ] [-Description ] [-AllowStickersAndMemes ] [-AllowTeamMentions ] [-AllowUserDeleteMessages ] [-AllowUserEditMessages ] [-GiphyContentRating ] [-Visibility ] - [-ShowInTeamsSearchAndSuggestions ] [-Classification ] + [-ShowInTeamsSearchAndSuggestions ] [-Classification ] + [-Owners ] [-Members ] [-ResourceBehaviorOptions ] [] ``` @@ -80,6 +81,13 @@ New-PnPTeamsTeam -DisplayName "myPnPDemo1" -Visibility Private -AllowCreateUpdat This will create a new Microsoft Teams 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 Microsoft Teams team will be added as Owner. Welcome Email will not be sent when the Group is created. The M365 Group will also not be visible in Outlook. +### EXAMPLE 5 +```powershell +New-PnPTeamsTeam -DisplayName "myPnPDemo1" -Visibility Private -Owners "user1@contoso.onmicrosoft.com","user2@contoso.onmicrosoft.com" -Members "user3@contoso.onmicrosoft.com" +``` + +This will create a new Microsoft Teams team called "myPnPDemo1" and sets the privacy to Private. User1 and user2 will be added as owners. User3 will be added as a member. + ## PARAMETERS ### -AllowAddRemoveApps @@ -419,6 +427,33 @@ Accept pipeline input: False Accept wildcard characters: False ``` +### -Owners +The UPN(s) of the user(s) to be added to the Microsoft 365 group as a owners. + +```yaml +Type: String[] +Parameter Sets: (All) + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Members +The UPN(s) of the user(s) to be added to the Microsoft 365 group as a members. + +```yaml +Type: String[] +Parameter Sets: (All) +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + ### -ResourceBehaviorOptions Allows providing ResourceBehaviorOptions which accepts multiple values that specify group behaviors for a Microsoft 365 Group. This will only work when you create a new Microsoft 365 Group, it will not work for existing groups. @@ -435,7 +470,6 @@ Accept pipeline input: False Accept wildcard characters: False ``` - ## RELATED LINKS [Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) diff --git a/src/Commands/Teams/NewTeamsTeam.cs b/src/Commands/Teams/NewTeamsTeam.cs index 5931d8dc9..aa8717f29 100644 --- a/src/Commands/Teams/NewTeamsTeam.cs +++ b/src/Commands/Teams/NewTeamsTeam.cs @@ -6,6 +6,7 @@ using PnP.PowerShell.Commands.Model.Teams; using PnP.PowerShell.Commands.Utilities; using System; +using System.Linq; using System.Management.Automation; namespace PnP.PowerShell.Commands.Graph @@ -31,6 +32,7 @@ public class NewTeamsTeam : PnPGraphCmdlet [ValidateLength(0, 1024)] public string Description; + [Obsolete("Please use the -Owners parameter instead. The -Owner parameter has been deprecated and will be removed in a future version.")] [Parameter(Mandatory = false, ParameterSetName = ParameterAttribute.AllParameterSets)] public string Owner; @@ -97,6 +99,12 @@ public class NewTeamsTeam : PnPGraphCmdlet [Parameter(Mandatory = false, ParameterSetName = ParameterAttribute.AllParameterSets)] public bool? AllowCreatePrivateChannels; + [Parameter(Mandatory = false, ParameterSetName = ParameterAttribute.AllParameterSets)] + public string[] Owners; + + [Parameter(Mandatory = false, ParameterSetName = ParameterAttribute.AllParameterSets)] + public string[] Members; + [Parameter(Mandatory = false, ParameterSetName = ParameterSet_NEWGROUP)] public TeamResourceBehaviorOptions?[] ResourceBehaviorOptions; @@ -126,9 +134,18 @@ protected override void ExecuteCmdlet() GroupId = GroupId, ShowInTeamsSearchAndSuggestions = ShowInTeamsSearchAndSuggestions, Visibility = (GroupVisibility)Enum.Parse(typeof(GroupVisibility), Visibility.ToString()), - AllowCreatePrivateChannels = AllowCreatePrivateChannels, + AllowCreatePrivateChannels = AllowCreatePrivateChannels }; - WriteObject(TeamsUtility.NewTeamAsync(AccessToken, HttpClient, GroupId, DisplayName, Description, Classification, MailNickName, Owner, (GroupVisibility)Enum.Parse(typeof(GroupVisibility), Visibility.ToString()), teamCI, Template, ResourceBehaviorOptions).GetAwaiter().GetResult()); + + #pragma warning disable 612, 618 // Disables the obsolete warning for the compiler output + if (!string.IsNullOrWhiteSpace(Owner)) + { + // Adding Owner parameter to the Owners array for backwards compatibility + Owners = Owners != null ? Owners.Concat(new[] { Owner }).ToArray() : new[] { Owner }; + } + #pragma warning restore 612, 618 + + WriteObject(TeamsUtility.NewTeamAsync(AccessToken, HttpClient, GroupId, DisplayName, Description, Classification, MailNickName, (GroupVisibility)Enum.Parse(typeof(GroupVisibility), Visibility.ToString()), teamCI, Owners, Members, Template, ResourceBehaviorOptions).GetAwaiter().GetResult()); } } } \ No newline at end of file diff --git a/src/Commands/Utilities/TeamsUtility.cs b/src/Commands/Utilities/TeamsUtility.cs index 16f0981ff..03418591a 100644 --- a/src/Commands/Utilities/TeamsUtility.cs +++ b/src/Commands/Utilities/TeamsUtility.cs @@ -1,4 +1,5 @@ -using PnP.PowerShell.Commands.Base; +using Microsoft.SharePoint.Client; +using PnP.PowerShell.Commands.Base; using PnP.PowerShell.Commands.Enums; using PnP.PowerShell.Commands.Model.Graph; using PnP.PowerShell.Commands.Model.Teams; @@ -10,6 +11,9 @@ using System.Net.Http; using System.Text.Json; using System.Threading.Tasks; +using Group = PnP.PowerShell.Commands.Model.Graph.Group; +using TeamChannel = PnP.PowerShell.Commands.Model.Teams.TeamChannel; +using User = PnP.PowerShell.Commands.Model.Teams.User; namespace PnP.PowerShell.Commands.Utilities { @@ -104,14 +108,15 @@ private static async Task ParseTeamJsonAsync(string accessToken, HttpClien } } - public static async Task NewTeamAsync(string accessToken, HttpClient httpClient, string groupId, string displayName, string description, string classification, string mailNickname, string owner, GroupVisibility visibility, TeamCreationInformation teamCI, TeamsTemplateType templateType = TeamsTemplateType.None, TeamResourceBehaviorOptions?[] resourceBehaviorOptions = null) + public static async Task NewTeamAsync(string accessToken, HttpClient httpClient, string groupId, string displayName, string description, string classification, string mailNickname, GroupVisibility visibility, TeamCreationInformation teamCI, string[] owners, string[] members, TeamsTemplateType templateType = TeamsTemplateType.None, TeamResourceBehaviorOptions?[] resourceBehaviorOptions = null) { Group group = null; Team returnTeam = null; - // Create group + + // Create the Group if (string.IsNullOrEmpty(groupId)) { - group = await CreateGroupAsync(accessToken, httpClient, displayName, description, classification, mailNickname, owner, visibility, templateType, resourceBehaviorOptions); + group = await CreateGroupAsync(accessToken, httpClient, displayName, description, classification, mailNickname, visibility, owners, templateType, resourceBehaviorOptions); bool wait = true; int iterations = 0; while (wait) @@ -161,7 +166,7 @@ public static async Task NewTeamAsync(string accessToken, HttpClient httpC var teamSettings = await GraphHelper.PutAsync(httpClient, $"v1.0/groups/{group.Id}/team", team, accessToken); if (teamSettings != null) { - returnTeam = await TeamsUtility.GetTeamAsync(accessToken, httpClient, group.Id); + returnTeam = await GetTeamAsync(accessToken, httpClient, group.Id); } retry = false; } @@ -177,51 +182,102 @@ public static async Task NewTeamAsync(string accessToken, HttpClient httpC retry = false; } } + + // Construct a list of all owners and members to add + var teamOwnersAndMembers = new List(); + if (owners != null && owners.Length > 0) + { + foreach (var owner in owners) + { + teamOwnersAndMembers.Add(new TeamChannelMember { Roles = new List { "owner" }, UserIdentifier = $"https://{PnPConnection.Current.GraphEndPoint}/v1.0/users('{owner}')" }); + } + } + + if (members != null && members.Length > 0) + { + foreach (var member in members) + { + teamOwnersAndMembers.Add(new TeamChannelMember { Roles = new List(), UserIdentifier = $"https://{PnPConnection.Current.GraphEndPoint}/v1.0/users('{member}')" }); + } + } + + if (teamOwnersAndMembers.Count > 0) + { + var ownersAndMembers = BatchUtility.Chunk(teamOwnersAndMembers, 200); + foreach (var chunk in ownersAndMembers) + { + await GraphHelper.PostAsync(httpClient, $"v1.0/teams/{group.Id}/members/add", new { values = chunk.ToList() }, accessToken); + } + } + } return returnTeam; } - private static async Task CreateGroupAsync(string accessToken, HttpClient httpClient, string displayName, string description, string classification, string mailNickname, string owner, GroupVisibility visibility, TeamsTemplateType templateType = TeamsTemplateType.None, TeamResourceBehaviorOptions?[] resourceBehaviorOptions = null) + private static async Task CreateGroupAsync(string accessToken, HttpClient httpClient, string displayName, string description, string classification, string mailNickname, GroupVisibility visibility, string[] owners, TeamsTemplateType templateType = TeamsTemplateType.None, TeamResourceBehaviorOptions?[] resourceBehaviorOptions = null) { - Group group = new Group(); - // get the owner if no owner was specified - var ownerId = string.Empty; - if (string.IsNullOrEmpty(owner)) - { - var user = await GraphHelper.GetAsync(httpClient, "v1.0/me?$select=Id", accessToken); - ownerId = user.Id; - } - else + // When creating a group, we always need an owner, thus we'll try to define it from the passed in owners array + string ownerId = null; + if (owners != null && owners.Length > 0) { - var user = await GraphHelper.GetAsync(httpClient, $"v1.0/users/{owner}?$select=Id", accessToken); + // Owner(s) have been provided, use the first owner as the initial owner. The other owners will be added later. + var user = await GraphHelper.GetAsync(httpClient, $"v1.0/users/{owners[0]}?$select=Id", accessToken); if (user != null) { + // User Id of the first owner has been found ownerId = user.Id; } else { - // find the user in the organization - var collection = await GraphHelper.GetResultCollectionAsync(httpClient, $"v1.0/users?$filter=mail eq '{owner}'&$select=Id", accessToken); - if (collection != null) + // Unable to find the owner by its user principal name, try looking for it on its email address + var collection = await GraphHelper.GetResultCollectionAsync(httpClient, $"v1.0/users?$filter=mail eq '{owners[0]}'&$select=Id", accessToken); + if (collection != null && collection.Any()) { - if (collection.Any()) - { - ownerId = collection.First().Id; - } + // User found on its email address + ownerId = collection.First().Id; + } + } + } + + // Check if by now we've identified a user Id to become the owner + if (!string.IsNullOrEmpty(ownerId)) + { + var contextSettings = PnPConnection.Current.Context.GetContextSettings(); + + // Still no owner identified, see if we can make the current user executing this cmdlet the owner + if (contextSettings.Type != Framework.Utilities.Context.ClientContextType.AzureADCertificate) + { + // A delegate context is available, make the user part of the delegate token the owner + var user = await GraphHelper.GetAsync(httpClient, "v1.0/me?$select=Id", accessToken); + + if (user != null) + { + // User executing the cmdlet will become the owner + ownerId = user.Id; } } } - group.DisplayName = displayName; - group.Description = description; - group.Classification = classification; - group.MailEnabled = true; - group.MailNickname = mailNickname ?? await CreateAliasAsync(httpClient, accessToken); - group.GroupTypes = new List() { "Unified" }; - group.SecurityEnabled = false; - group.Owners = new List() { $"https://{PnPConnection.Current.GraphEndPoint}/v1.0/users/{ownerId}" }; - group.Members = new List() { $"https://{PnPConnection.Current.GraphEndPoint}/v1.0/users/{ownerId}" }; - group.Visibility = visibility == GroupVisibility.NotSpecified ? GroupVisibility.Private : visibility; + // Construct the new group + Group group = new Group + { + DisplayName = displayName, + Description = description, + Classification = classification, + MailEnabled = true, + MailNickname = mailNickname ?? await CreateAliasAsync(httpClient, accessToken), + GroupTypes = new List() { "Unified" }, + SecurityEnabled = false, + Visibility = visibility == GroupVisibility.NotSpecified ? GroupVisibility.Private : visibility + }; + + // Check if we managed to define an owner for the group. If not, we'll revert to not providing an owner, which will mean that the app principal will become the owner of the Group + if (!string.IsNullOrEmpty(ownerId)) + { + group.Owners = new List() { $"https://{PnPConnection.Current.GraphEndPoint}/v1.0/users/{ownerId}" }; + group.Members = new List() { $"https://{PnPConnection.Current.GraphEndPoint}/v1.0/users/{ownerId}" }; + } + if (resourceBehaviorOptions != null && resourceBehaviorOptions.Length > 0) { var teamResourceBehaviorOptionsValue = new List(); @@ -235,22 +291,18 @@ private static async Task CreateGroupAsync(string accessToken, HttpClient switch (templateType) { case TeamsTemplateType.EDU_Class: - { - group.Visibility = GroupVisibility.HiddenMembership; - group.CreationOptions = new List { "ExchangeProvisioningFlags:461", "classAssignments" }; - group.EducationObjectType = "Section"; - break; - } + group.Visibility = GroupVisibility.HiddenMembership; + group.CreationOptions = new List { "ExchangeProvisioningFlags:461", "classAssignments" }; + group.EducationObjectType = "Section"; + break; + case TeamsTemplateType.EDU_PLC: - { - group.CreationOptions = new List { "PLC" }; - break; - } + group.CreationOptions = new List { "PLC" }; + break; + default: - { - group.CreationOptions = new List { "ExchangeProvisioningFlags:3552" }; - break; - } + group.CreationOptions = new List { "ExchangeProvisioningFlags:3552" }; + break; } try { @@ -267,7 +319,6 @@ private static async Task CreateGroupAsync(string accessToken, HttpClient throw; } } - } private static async Task CreateAliasAsync(HttpClient httpClient, string accessToken)