From 21e97274d970ba88a1c3b331d8c3b16544e0cb47 Mon Sep 17 00:00:00 2001 From: Gautam Sheth Date: Tue, 12 Oct 2021 22:29:42 +0300 Subject: [PATCH 01/24] #1021 , #1022 - added ability to add multiple owners and members --- documentation/New-PnPTeamsTeam.md | 31 +++++++++++++++++++++- src/Commands/Teams/NewTeamsTeam.cs | 10 +++++-- src/Commands/Utilities/TeamsUtility.cs | 36 +++++++++++++++++++------- 3 files changed, 65 insertions(+), 12 deletions(-) diff --git a/documentation/New-PnPTeamsTeam.md b/documentation/New-PnPTeamsTeam.md index 10332fda2..44b988ac6 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 @@ -43,6 +43,7 @@ New-PnPTeamsTeam -DisplayName [-MailNickName ] [-Description ] [-AllowUserEditMessages ] [-GiphyContentRating ] [-Visibility ] [-ShowInTeamsSearchAndSuggestions ] [-Classification ] + [-Owners ] [-Members ] [] ``` @@ -403,6 +404,34 @@ 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 +``` + ## 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 b516494ea..b8a61e75c 100644 --- a/src/Commands/Teams/NewTeamsTeam.cs +++ b/src/Commands/Teams/NewTeamsTeam.cs @@ -95,7 +95,13 @@ public class NewTeamsTeam : PnPGraphCmdlet public TeamsTemplateType Template = TeamsTemplateType.None; [Parameter(Mandatory = false, ParameterSetName = ParameterAttribute.AllParameterSets)] - public bool? AllowCreatePrivateChannels; + public bool? AllowCreatePrivateChannels; + + [Parameter(Mandatory = false, ParameterSetName = ParameterAttribute.AllParameterSets)] + public string[] Owners; + + [Parameter(Mandatory = false, ParameterSetName = ParameterAttribute.AllParameterSets)] + public string[] Members; protected override void ExecuteCmdlet() { @@ -125,7 +131,7 @@ protected override void ExecuteCmdlet() Visibility = (GroupVisibility)Enum.Parse(typeof(GroupVisibility), Visibility.ToString()), AllowCreatePrivateChannels = AllowCreatePrivateChannels, }; - WriteObject(TeamsUtility.NewTeamAsync(AccessToken, HttpClient, GroupId, DisplayName, Description, Classification, MailNickName, Owner, (GroupVisibility)Enum.Parse(typeof(GroupVisibility), Visibility.ToString()), teamCI, Template).GetAwaiter().GetResult()); + WriteObject(TeamsUtility.NewTeamAsync(AccessToken, HttpClient, GroupId, DisplayName, Description, Classification, MailNickName, Owner, (GroupVisibility)Enum.Parse(typeof(GroupVisibility), Visibility.ToString()), teamCI, Owners, Members, Template).GetAwaiter().GetResult()); } } } \ No newline at end of file diff --git a/src/Commands/Utilities/TeamsUtility.cs b/src/Commands/Utilities/TeamsUtility.cs index 1fd09388b..bcabe78b7 100644 --- a/src/Commands/Utilities/TeamsUtility.cs +++ b/src/Commands/Utilities/TeamsUtility.cs @@ -59,7 +59,7 @@ public static async Task GetTeamAsync(string accessToken, HttpClient httpC { team.DisplayName = group.DisplayName; team.MailNickname = group.MailNickname; - team.Visibility = group.Visibility; + team.Visibility = group.Visibility; return team; } else @@ -104,14 +104,14 @@ 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) + 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, string[] owners, string[] members, TeamsTemplateType templateType = TeamsTemplateType.None) { Group group = null; Team returnTeam = null; // Create group if (string.IsNullOrEmpty(groupId)) { - group = await CreateGroupAsync(accessToken, httpClient, displayName, description, classification, mailNickname, owner, visibility, templateType); + group = await CreateGroupAsync(accessToken, httpClient, displayName, description, classification, mailNickname, owner, visibility, owners, templateType); bool wait = true; int iterations = 0; while (wait) @@ -151,6 +151,16 @@ public static async Task NewTeamAsync(string accessToken, HttpClient httpC } if (group != null) { + if (owners != null && owners.Length > 0) + { + Framework.Graph.GroupsUtility.AddGroupOwners(group.Id, owners, accessToken, false); + } + + if (members != null && members.Length > 0) + { + Framework.Graph.GroupsUtility.AddGroupMembers(group.Id, members, accessToken, false); + } + Team team = teamCI.ToTeam(group.Visibility); var retry = true; var iteration = 0; @@ -161,7 +171,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; } @@ -181,7 +191,7 @@ public static async Task NewTeamAsync(string accessToken, HttpClient httpC 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) + private static async Task CreateGroupAsync(string accessToken, HttpClient httpClient, string displayName, string description, string classification, string mailNickname, string owner, GroupVisibility visibility, string[] owners, TeamsTemplateType templateType = TeamsTemplateType.None) { Group group = new Group(); // get the owner if no owner was specified @@ -191,6 +201,11 @@ private static async Task CreateGroupAsync(string accessToken, HttpClient var user = await GraphHelper.GetAsync(httpClient, "v1.0/me?$select=Id", accessToken); ownerId = user.Id; } + else if(owners !=null && owners.Length > 0) + { + var user = await GraphHelper.GetAsync(httpClient, $"v1.0/users/{owners[0]}?$select=Id", accessToken); + ownerId = user.Id; + } else { var user = await GraphHelper.GetAsync(httpClient, $"v1.0/users/{owner}?$select=Id", accessToken); @@ -246,12 +261,15 @@ private static async Task CreateGroupAsync(string accessToken, HttpClient try { return await GraphHelper.PostAsync(httpClient, "v1.0/groups", group, accessToken); - } catch (GraphException ex) + } + catch (GraphException ex) { - if(ex.Error.Message.Contains("extension_fe2174665583431c953114ff7268b7b3_Education_ObjectType")) + if (ex.Error.Message.Contains("extension_fe2174665583431c953114ff7268b7b3_Education_ObjectType")) { throw new PSInvalidOperationException("Invalid EDU license type"); - } else { + } + else + { throw; } } @@ -396,7 +414,7 @@ public static async Task> GetUsersAsync(HttpClient httpClient, { users.AddRange(collection.Select(m => new User() { DisplayName = m.DisplayName, Id = m.UserId, UserPrincipalName = m.email, UserType = m.Roles.Count > 0 ? m.Roles[0].ToLower() : "" })); } - + if (selectedRole != null) { return users.Where(u => u.UserType == selectedRole); From 330065ef426a99fc1619f475a39f361760dd4e6d Mon Sep 17 00:00:00 2001 From: Gautam Sheth Date: Tue, 12 Oct 2021 22:34:38 +0300 Subject: [PATCH 02/24] Empty owner logic change --- src/Commands/Utilities/TeamsUtility.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Commands/Utilities/TeamsUtility.cs b/src/Commands/Utilities/TeamsUtility.cs index bcabe78b7..ec4a6cb8c 100644 --- a/src/Commands/Utilities/TeamsUtility.cs +++ b/src/Commands/Utilities/TeamsUtility.cs @@ -198,13 +198,16 @@ private static async Task CreateGroupAsync(string accessToken, HttpClient var ownerId = string.Empty; if (string.IsNullOrEmpty(owner)) { - var user = await GraphHelper.GetAsync(httpClient, "v1.0/me?$select=Id", accessToken); - ownerId = user.Id; - } - else if(owners !=null && owners.Length > 0) - { - var user = await GraphHelper.GetAsync(httpClient, $"v1.0/users/{owners[0]}?$select=Id", accessToken); - ownerId = user.Id; + if (owners != null && owners.Length > 0) + { + var user = await GraphHelper.GetAsync(httpClient, $"v1.0/users/{owners[0]}?$select=Id", accessToken); + ownerId = user.Id; + } + else + { + var user = await GraphHelper.GetAsync(httpClient, "v1.0/me?$select=Id", accessToken); + ownerId = user.Id; + } } else { From f2f96152bbabda57733af0e58ffe658b6616e4d0 Mon Sep 17 00:00:00 2001 From: Gautam Sheth Date: Tue, 12 Oct 2021 22:39:54 +0300 Subject: [PATCH 03/24] Added changelog entry --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 055b5bc4b..bd45002c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,11 +14,13 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). - Added optional `-ScheduledPublishDate` parameter to `Add-PnPPage` and `Set-PnPPage` to allow for scheduling a page to be published. - Added `-RemoveScheduledPublish` to `Set-PnPPage` to allow for a page publish schedule to be removed. - Added support for off peak SharePoint Syntex content classification and extraction for lists and folders via new `-OffPeak` and `-Folder` parameters for `Request-PnPSyntexClassifyAndExtract` +- Added support to add multiple owners and members in `New-PnPTeamsTeam` cmdlet. ### Contributors - Koen Zomers [koenzomers] - Bert Jansen [jansenbe] +- Gautam Sheth [gautamdsheth] ## [1.8.0] From 433069499e8ab2863a77084d3d791d15a24f5832 Mon Sep 17 00:00:00 2001 From: Gautam Sheth Date: Sun, 17 Oct 2021 21:45:11 +0300 Subject: [PATCH 04/24] Implement batch add of members and owners --- src/Commands/Utilities/TeamsUtility.cs | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/Commands/Utilities/TeamsUtility.cs b/src/Commands/Utilities/TeamsUtility.cs index ec4a6cb8c..bcb5cdc26 100644 --- a/src/Commands/Utilities/TeamsUtility.cs +++ b/src/Commands/Utilities/TeamsUtility.cs @@ -153,12 +153,32 @@ public static async Task NewTeamAsync(string accessToken, HttpClient httpC { if (owners != null && owners.Length > 0) { - Framework.Graph.GroupsUtility.AddGroupOwners(group.Id, owners, accessToken, false); + var chunks = BatchUtility.Chunk(owners, 20); + foreach (var chunk in chunks) + { + var results = await BatchUtility.GetPropertyBatchedAsync(httpClient, accessToken, chunk.ToArray(), "/users/{0}", "id"); + var teamOwners = new List(); + foreach (var userid in results.Select(r => r.Value)) + { + teamOwners.Add(new TeamChannelMember() { Roles = new List { "owner" }, UserIdentifier = $"https://{PnPConnection.Current.GraphEndPoint}/v1.0/users('{userid}')" }); + } + await GraphHelper.PostAsync(httpClient, $"v1.0/teams/{groupId}/members/add", new { values = teamOwners }, accessToken); + } } if (members != null && members.Length > 0) { - Framework.Graph.GroupsUtility.AddGroupMembers(group.Id, members, accessToken, false); + var chunks = BatchUtility.Chunk(members, 20); + foreach (var chunk in chunks) + { + var results = await BatchUtility.GetPropertyBatchedAsync(httpClient, accessToken, chunk.ToArray(), "/users/{0}", "id"); + var teamMembers = new List(); + foreach (var userid in results.Select(r => r.Value)) + { + teamMembers.Add(new TeamChannelMember() { Roles = new List { "member" }, UserIdentifier = $"https://{PnPConnection.Current.GraphEndPoint}/v1.0/users('{userid}')" }); + } + await GraphHelper.PostAsync(httpClient, $"v1.0/teams/{groupId}/members/add", new { values = teamMembers }, accessToken); + } } Team team = teamCI.ToTeam(group.Visibility); From 3e62dd30eb69bcc28932bf26470f0afed4c095d8 Mon Sep 17 00:00:00 2001 From: Gautam Sheth Date: Sun, 14 Nov 2021 20:29:04 +0200 Subject: [PATCH 05/24] Fixed merge changes --- src/Commands/Teams/NewTeamsTeam.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Commands/Teams/NewTeamsTeam.cs b/src/Commands/Teams/NewTeamsTeam.cs index 4835c96cc..3bfec008d 100644 --- a/src/Commands/Teams/NewTeamsTeam.cs +++ b/src/Commands/Teams/NewTeamsTeam.cs @@ -102,6 +102,7 @@ public class NewTeamsTeam : PnPGraphCmdlet [Parameter(Mandatory = false, ParameterSetName = ParameterAttribute.AllParameterSets)] public string[] Members; + [Parameter(Mandatory = false, ParameterSetName = ParameterSet_NEWGROUP)] public TeamResourceBehaviorOptions?[] ResourceBehaviorOptions; @@ -133,7 +134,7 @@ protected override void ExecuteCmdlet() Visibility = (GroupVisibility)Enum.Parse(typeof(GroupVisibility), Visibility.ToString()), AllowCreatePrivateChannels = AllowCreatePrivateChannels, }; - WriteObject(TeamsUtility.NewTeamAsync(AccessToken, HttpClient, GroupId, DisplayName, Description, Classification, MailNickName, Owner, (GroupVisibility)Enum.Parse(typeof(GroupVisibility), Visibility.ToString()), teamCI, Owners, Members, Template).GetAwaiter().GetResult()); + WriteObject(TeamsUtility.NewTeamAsync(AccessToken, HttpClient, GroupId, DisplayName, Description, Classification, MailNickName, Owner, (GroupVisibility)Enum.Parse(typeof(GroupVisibility), Visibility.ToString()), teamCI, Owners, Members, Template, ResourceBehaviorOptions).GetAwaiter().GetResult()); } } } \ No newline at end of file From b6666947bf5ef7c0e27668d9c7cccd61812a46b1 Mon Sep 17 00:00:00 2001 From: Gautam Sheth Date: Tue, 14 Dec 2021 08:26:49 +0200 Subject: [PATCH 06/24] Fixes as per review comments --- src/Commands/Teams/NewTeamsTeam.cs | 1 + src/Commands/Utilities/TeamsUtility.cs | 71 +++++++++++++++++++++----- 2 files changed, 60 insertions(+), 12 deletions(-) diff --git a/src/Commands/Teams/NewTeamsTeam.cs b/src/Commands/Teams/NewTeamsTeam.cs index 3bfec008d..c853b07b2 100644 --- a/src/Commands/Teams/NewTeamsTeam.cs +++ b/src/Commands/Teams/NewTeamsTeam.cs @@ -31,6 +31,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; diff --git a/src/Commands/Utilities/TeamsUtility.cs b/src/Commands/Utilities/TeamsUtility.cs index 88f7b2987..7478bb23b 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 { @@ -216,35 +220,78 @@ private static async Task CreateGroupAsync(string accessToken, HttpClient Group group = new Group(); // get the owner if no owner was specified var ownerId = string.Empty; + var contextSettings = PnPConnection.Current.Context.GetContextSettings(); if (string.IsNullOrEmpty(owner)) { if (owners != null && owners.Length > 0) { var user = await GraphHelper.GetAsync(httpClient, $"v1.0/users/{owners[0]}?$select=Id", accessToken); - ownerId = user.Id; + if (user != null) + { + ownerId = user.Id; + } + else + { + // find the user in the organization + var collection = await GraphHelper.GetResultCollectionAsync(httpClient, $"v1.0/users?$filter=mail eq '{owner[0]}'&$select=Id", accessToken); + if (collection != null) + { + if (collection.Any()) + { + ownerId = collection.First().Id; + } + } + } } else { - var user = await GraphHelper.GetAsync(httpClient, "v1.0/me?$select=Id", accessToken); - ownerId = user.Id; + if (contextSettings.Type != Framework.Utilities.Context.ClientContextType.AzureADCertificate) + { + var user = await GraphHelper.GetAsync(httpClient, "v1.0/me?$select=Id", accessToken); + ownerId = user.Id; + } } } else { - var user = await GraphHelper.GetAsync(httpClient, $"v1.0/users/{owner}?$select=Id", accessToken); - if (user != null) + if (owners != null && owners.Length > 0) { - ownerId = user.Id; + var user = await GraphHelper.GetAsync(httpClient, $"v1.0/users/{owners[0]}?$select=Id", accessToken); + if (user != null) + { + ownerId = user.Id; + } + else + { + // find the user in the organization + var collection = await GraphHelper.GetResultCollectionAsync(httpClient, $"v1.0/users?$filter=mail eq '{owner[0]}'&$select=Id", accessToken); + if (collection != null) + { + if (collection.Any()) + { + ownerId = collection.First().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) + // To do : remove this entire else condition at a future date + var user = await GraphHelper.GetAsync(httpClient, $"v1.0/users/{owner}?$select=Id", accessToken); + if (user != null) + { + ownerId = user.Id; + } + else { - if (collection.Any()) + // 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) { - ownerId = collection.First().Id; + if (collection.Any()) + { + ownerId = collection.First().Id; + } } } } From b556498deea72dc5bb70645d84c7213c65af8d7b Mon Sep 17 00:00:00 2001 From: Gautam Sheth Date: Tue, 14 Dec 2021 12:44:19 +0200 Subject: [PATCH 07/24] Updated docs form Set-PnPTenant --- documentation/Set-PnPTenant.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/documentation/Set-PnPTenant.md b/documentation/Set-PnPTenant.md index 2c11d4997..224c0b156 100644 --- a/documentation/Set-PnPTenant.md +++ b/documentation/Set-PnPTenant.md @@ -1254,6 +1254,20 @@ Accept pipeline input: False Accept wildcard characters: False ``` +### -IsFluidEnabled +Allows configuration on whether Fluid components are enabled or disabled in the tenant. If set to `$true`, then this feature will be disabled on all sites in the tenant. If set to `$false`, it will be enabled on all sites in the tenant. + +```yaml +Type: Boolean +Parameter Sets: (All) +Aliases: DisableAddShortcutsToOneDrive +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + ## RELATED LINKS [Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) From fc0d9f255990cecc6f2814559fa636397693b277 Mon Sep 17 00:00:00 2001 From: Gautam Sheth Date: Tue, 14 Dec 2021 12:46:23 +0200 Subject: [PATCH 08/24] Undo docs update --- documentation/Set-PnPTenant.md | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/documentation/Set-PnPTenant.md b/documentation/Set-PnPTenant.md index 224c0b156..2c11d4997 100644 --- a/documentation/Set-PnPTenant.md +++ b/documentation/Set-PnPTenant.md @@ -1254,20 +1254,6 @@ Accept pipeline input: False Accept wildcard characters: False ``` -### -IsFluidEnabled -Allows configuration on whether Fluid components are enabled or disabled in the tenant. If set to `$true`, then this feature will be disabled on all sites in the tenant. If set to `$false`, it will be enabled on all sites in the tenant. - -```yaml -Type: Boolean -Parameter Sets: (All) -Aliases: DisableAddShortcutsToOneDrive -Required: False -Position: Named -Default value: None -Accept pipeline input: False -Accept wildcard characters: False -``` - ## RELATED LINKS [Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) From 7574489eb7e4154e092340d9fd1c126b14022d6f Mon Sep 17 00:00:00 2001 From: Koen Zomers Date: Tue, 21 Dec 2021 01:02:45 +0100 Subject: [PATCH 09/24] Rebuilt logic --- documentation/New-PnPTeamsTeam.md | 7 ++ src/Commands/Teams/NewTeamsTeam.cs | 16 ++- src/Commands/Utilities/TeamsUtility.cs | 160 ++++++++----------------- 3 files changed, 72 insertions(+), 111 deletions(-) diff --git a/documentation/New-PnPTeamsTeam.md b/documentation/New-PnPTeamsTeam.md index 0554f5c4d..3a11c2bdc 100644 --- a/documentation/New-PnPTeamsTeam.md +++ b/documentation/New-PnPTeamsTeam.md @@ -81,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 diff --git a/src/Commands/Teams/NewTeamsTeam.cs b/src/Commands/Teams/NewTeamsTeam.cs index c853b07b2..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,7 +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. ")] + [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; @@ -133,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, Owners, Members, 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 9b105c961..bc5d669d2 100644 --- a/src/Commands/Utilities/TeamsUtility.cs +++ b/src/Commands/Utilities/TeamsUtility.cs @@ -108,14 +108,14 @@ 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, string[] owners, string[] members, 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 if (string.IsNullOrEmpty(groupId)) { - group = await CreateGroupAsync(accessToken, httpClient, displayName, description, classification, mailNickname, owner, visibility, owners, templateType, resourceBehaviorOptions); + group = await CreateGroupAsync(accessToken, httpClient, displayName, description, classification, mailNickname, visibility, owners, templateType, resourceBehaviorOptions); bool wait = true; int iterations = 0; while (wait) @@ -155,36 +155,6 @@ public static async Task NewTeamAsync(string accessToken, HttpClient httpC } if (group != null) { - if (owners != null && owners.Length > 0) - { - var chunks = BatchUtility.Chunk(owners, 20); - foreach (var chunk in chunks) - { - var results = await BatchUtility.GetPropertyBatchedAsync(httpClient, accessToken, chunk.ToArray(), "/users/{0}", "id"); - var teamOwners = new List(); - foreach (var userid in results.Select(r => r.Value)) - { - teamOwners.Add(new TeamChannelMember() { Roles = new List { "owner" }, UserIdentifier = $"https://{PnPConnection.Current.GraphEndPoint}/v1.0/users('{userid}')" }); - } - await GraphHelper.PostAsync(httpClient, $"v1.0/teams/{groupId}/members/add", new { values = teamOwners }, accessToken); - } - } - - if (members != null && members.Length > 0) - { - var chunks = BatchUtility.Chunk(members, 20); - foreach (var chunk in chunks) - { - var results = await BatchUtility.GetPropertyBatchedAsync(httpClient, accessToken, chunk.ToArray(), "/users/{0}", "id"); - var teamMembers = new List(); - foreach (var userid in results.Select(r => r.Value)) - { - teamMembers.Add(new TeamChannelMember() { Roles = new List { "member" }, UserIdentifier = $"https://{PnPConnection.Current.GraphEndPoint}/v1.0/users('{userid}')" }); - } - await GraphHelper.PostAsync(httpClient, $"v1.0/teams/{groupId}/members/add", new { values = teamMembers }, accessToken); - } - } - Team team = teamCI.ToTeam(group.Visibility); var retry = true; var iteration = 0; @@ -211,87 +181,66 @@ public static async Task NewTeamAsync(string accessToken, HttpClient httpC retry = false; } } - } - return returnTeam; - } - private static async Task CreateGroupAsync(string accessToken, HttpClient httpClient, string displayName, string description, string classification, string mailNickname, string owner, 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; - var contextSettings = PnPConnection.Current.Context.GetContextSettings(); - if (string.IsNullOrEmpty(owner)) - { + var teamMembers = new List(); if (owners != null && owners.Length > 0) { - var user = await GraphHelper.GetAsync(httpClient, $"v1.0/users/{owners[0]}?$select=Id", accessToken); - if (user != null) - { - ownerId = user.Id; - } - else + var chunks = BatchUtility.Chunk(owners, 20); + foreach (var chunk in chunks) { - // find the user in the organization - var collection = await GraphHelper.GetResultCollectionAsync(httpClient, $"v1.0/users?$filter=mail eq '{owner[0]}'&$select=Id", accessToken); - if (collection != null) + var results = await BatchUtility.GetPropertyBatchedAsync(httpClient, accessToken, chunk.ToArray(), "/users/{0}", "id"); + + foreach (var userid in results.Select(r => r.Value)) { - if (collection.Any()) - { - ownerId = collection.First().Id; - } + teamMembers.Add(new TeamChannelMember { Roles = new List { "owner" }, UserIdentifier = $"https://{PnPConnection.Current.GraphEndPoint}/v1.0/users('{userid}')" }); } } } - else + + if (members != null && members.Length > 0) { - if (contextSettings.Type != Framework.Utilities.Context.ClientContextType.AzureADCertificate) + var chunks = BatchUtility.Chunk(members, 20); + foreach (var chunk in chunks) { - var user = await GraphHelper.GetAsync(httpClient, "v1.0/me?$select=Id", accessToken); - ownerId = user.Id; + var results = await BatchUtility.GetPropertyBatchedAsync(httpClient, accessToken, chunk.ToArray(), "/users/{0}", "id"); + + foreach (var userid in results.Select(r => r.Value)) + { + teamMembers.Add(new TeamChannelMember { Roles = new List { "member" }, UserIdentifier = $"https://{PnPConnection.Current.GraphEndPoint}/v1.0/users('{userid}')" }); + } } } + + if (teamMembers.Count > 0) + { + await GraphHelper.PostAsync(httpClient, $"v1.0/teams/{group.Id}/members/add", new { values = teamMembers }, accessToken); + } } - else + return returnTeam; + } + + 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; + var contextSettings = PnPConnection.Current.Context.GetContextSettings(); + if (owners != null && owners.Length > 0) { - if (owners != null && owners.Length > 0) + var user = await GraphHelper.GetAsync(httpClient, $"v1.0/users/{owners[0]}?$select=Id", accessToken); + if (user != null) { - var user = await GraphHelper.GetAsync(httpClient, $"v1.0/users/{owners[0]}?$select=Id", accessToken); - if (user != null) - { - ownerId = user.Id; - } - else - { - // find the user in the organization - var collection = await GraphHelper.GetResultCollectionAsync(httpClient, $"v1.0/users?$filter=mail eq '{owner[0]}'&$select=Id", accessToken); - if (collection != null) - { - if (collection.Any()) - { - ownerId = collection.First().Id; - } - } - } + ownerId = user.Id; } else { - // To do : remove this entire else condition at a future date - var user = await GraphHelper.GetAsync(httpClient, $"v1.0/users/{owner}?$select=Id", accessToken); - if (user != null) - { - ownerId = user.Id; - } - else + // find the user in the organization + var collection = await GraphHelper.GetResultCollectionAsync(httpClient, $"v1.0/users?$filter=mail eq '{owners[0]}'&$select=Id", accessToken); + if (collection != null) { - // 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) + if (collection.Any()) { - if (collection.Any()) - { - ownerId = collection.First().Id; - } + ownerId = collection.First().Id; } } } @@ -320,22 +269,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 { @@ -352,7 +297,6 @@ private static async Task CreateGroupAsync(string accessToken, HttpClient throw; } } - } private static async Task CreateAliasAsync(HttpClient httpClient, string accessToken) From 3f5df81f05a82afe4d8e1a695904f51c80f03011 Mon Sep 17 00:00:00 2001 From: Gautam Sheth Date: Tue, 21 Dec 2021 08:41:28 +0200 Subject: [PATCH 10/24] Fixed chunk add logic, Graph only allows 20 users at a time --- src/Commands/Utilities/TeamsUtility.cs | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/Commands/Utilities/TeamsUtility.cs b/src/Commands/Utilities/TeamsUtility.cs index bc5d669d2..34cb1ca30 100644 --- a/src/Commands/Utilities/TeamsUtility.cs +++ b/src/Commands/Utilities/TeamsUtility.cs @@ -182,18 +182,22 @@ public static async Task NewTeamAsync(string accessToken, HttpClient httpC } } - var teamMembers = new List(); if (owners != null && owners.Length > 0) { var chunks = BatchUtility.Chunk(owners, 20); foreach (var chunk in chunks) { + var teamMembers = new List(); var results = await BatchUtility.GetPropertyBatchedAsync(httpClient, accessToken, chunk.ToArray(), "/users/{0}", "id"); - + foreach (var userid in results.Select(r => r.Value)) { teamMembers.Add(new TeamChannelMember { Roles = new List { "owner" }, UserIdentifier = $"https://{PnPConnection.Current.GraphEndPoint}/v1.0/users('{userid}')" }); } + if (teamMembers.Count > 0) + { + await GraphHelper.PostAsync(httpClient, $"v1.0/teams/{group.Id}/members/add", new { values = teamMembers }, accessToken); + } } } @@ -202,19 +206,19 @@ public static async Task NewTeamAsync(string accessToken, HttpClient httpC var chunks = BatchUtility.Chunk(members, 20); foreach (var chunk in chunks) { - var results = await BatchUtility.GetPropertyBatchedAsync(httpClient, accessToken, chunk.ToArray(), "/users/{0}", "id"); - + var teamMembers = new List(); + var results = await BatchUtility.GetPropertyBatchedAsync(httpClient, accessToken, chunk.ToArray(), "/users/{0}", "id"); + foreach (var userid in results.Select(r => r.Value)) { teamMembers.Add(new TeamChannelMember { Roles = new List { "member" }, UserIdentifier = $"https://{PnPConnection.Current.GraphEndPoint}/v1.0/users('{userid}')" }); } + if (teamMembers.Count > 0) + { + await GraphHelper.PostAsync(httpClient, $"v1.0/teams/{group.Id}/members/add", new { values = teamMembers }, accessToken); + } } } - - if (teamMembers.Count > 0) - { - await GraphHelper.PostAsync(httpClient, $"v1.0/teams/{group.Id}/members/add", new { values = teamMembers }, accessToken); - } } return returnTeam; } From 3e99cea811dae0f4352a698bcc36a7e7d236a87c Mon Sep 17 00:00:00 2001 From: Gautam Sheth Date: Tue, 21 Dec 2021 08:48:32 +0200 Subject: [PATCH 11/24] Updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a58331c1..f03e87b3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,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 From 0ffe0d3f889f82c9fe5ec9fbacd8ad337b42a4a3 Mon Sep 17 00:00:00 2001 From: Gautam Sheth Date: Tue, 21 Dec 2021 08:49:04 +0200 Subject: [PATCH 12/24] Author credit --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f03e87b3f..3d9b41467 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,6 +82,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] From c7879acf4aa79fd007359dbff5a388db1e26906c Mon Sep 17 00:00:00 2001 From: Gautam Sheth Date: Wed, 22 Dec 2021 10:11:01 +0200 Subject: [PATCH 13/24] Fix handling owner if not specified --- src/Commands/Utilities/TeamsUtility.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Commands/Utilities/TeamsUtility.cs b/src/Commands/Utilities/TeamsUtility.cs index 34cb1ca30..8c2218fb2 100644 --- a/src/Commands/Utilities/TeamsUtility.cs +++ b/src/Commands/Utilities/TeamsUtility.cs @@ -228,7 +228,6 @@ private static async Task CreateGroupAsync(string accessToken, HttpClient Group group = new Group(); // get the owner if no owner was specified var ownerId = string.Empty; - var contextSettings = PnPConnection.Current.Context.GetContextSettings(); if (owners != null && owners.Length > 0) { var user = await GraphHelper.GetAsync(httpClient, $"v1.0/users/{owners[0]}?$select=Id", accessToken); @@ -249,6 +248,14 @@ private static async Task CreateGroupAsync(string accessToken, HttpClient } } } + else + { + var user = await GraphHelper.GetAsync(httpClient, "v1.0/me?$select=Id", accessToken); + if(user != null) + { + ownerId = user.Id; + } + } group.DisplayName = displayName; group.Description = description; @@ -257,8 +264,11 @@ private static async Task CreateGroupAsync(string accessToken, HttpClient 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}" }; + 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}" }; + } group.Visibility = visibility == GroupVisibility.NotSpecified ? GroupVisibility.Private : visibility; if (resourceBehaviorOptions != null && resourceBehaviorOptions.Length > 0) { From 2d4817232d3b0fa7dd05993b29522f40d20cf60b Mon Sep 17 00:00:00 2001 From: Gautam Sheth Date: Wed, 22 Dec 2021 10:14:37 +0200 Subject: [PATCH 14/24] whitespace change --- src/Commands/Utilities/TeamsUtility.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Commands/Utilities/TeamsUtility.cs b/src/Commands/Utilities/TeamsUtility.cs index 8c2218fb2..c000bd590 100644 --- a/src/Commands/Utilities/TeamsUtility.cs +++ b/src/Commands/Utilities/TeamsUtility.cs @@ -251,10 +251,10 @@ private static async Task CreateGroupAsync(string accessToken, HttpClient else { var user = await GraphHelper.GetAsync(httpClient, "v1.0/me?$select=Id", accessToken); - if(user != null) + if (user != null) { ownerId = user.Id; - } + } } group.DisplayName = displayName; From 1405bfb9fbdc2f631e3da83ee9dd1eeb4eaec09c Mon Sep 17 00:00:00 2001 From: Koen Zomers Date: Wed, 22 Dec 2021 12:53:41 +0100 Subject: [PATCH 15/24] Updating code to better handle not providing owners --- src/Commands/Utilities/TeamsUtility.cs | 57 +++++++++++++++++--------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/src/Commands/Utilities/TeamsUtility.cs b/src/Commands/Utilities/TeamsUtility.cs index 34cb1ca30..82849dec2 100644 --- a/src/Commands/Utilities/TeamsUtility.cs +++ b/src/Commands/Utilities/TeamsUtility.cs @@ -225,41 +225,58 @@ public static async Task NewTeamAsync(string accessToken, HttpClient httpC 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; - var contextSettings = PnPConnection.Current.Context.GetContextSettings(); + // 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) { + // 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 + // 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) + 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)) + { + // TODO Koen: add a check on how we are logged on and if we can call the /me endpoint using that (i.e. won't work under appcreds) + + // Still no owner identified, see if we can make the current user executing this cmdlet 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 group = new Group + { + DisplayName = displayName, + Description = description, + Classification = classification, + MailEnabled = true, + MailNickname = mailNickname ?? await CreateAliasAsync(httpClient, accessToken), + GroupTypes = new List() { "Unified" }, + SecurityEnabled = false, + Owners = new List() { $"https://{PnPConnection.Current.GraphEndPoint}/v1.0/users/{ownerId}" }, + //group.Members = new List() { $"https://{PnPConnection.Current.GraphEndPoint}/v1.0/users/{ownerId}" }; + Visibility = visibility == GroupVisibility.NotSpecified ? GroupVisibility.Private : visibility + }; - 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; if (resourceBehaviorOptions != null && resourceBehaviorOptions.Length > 0) { var teamResourceBehaviorOptionsValue = new List(); From e50ded900293cee5e8c0d385fa2a86a7e3bdde55 Mon Sep 17 00:00:00 2001 From: Koen Zomers Date: Wed, 22 Dec 2021 12:55:20 +0100 Subject: [PATCH 16/24] Adding todo to check on adding owners as members --- src/Commands/Utilities/TeamsUtility.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Commands/Utilities/TeamsUtility.cs b/src/Commands/Utilities/TeamsUtility.cs index 82849dec2..894fe2936 100644 --- a/src/Commands/Utilities/TeamsUtility.cs +++ b/src/Commands/Utilities/TeamsUtility.cs @@ -252,7 +252,7 @@ private static async Task CreateGroupAsync(string accessToken, HttpClient if(!string.IsNullOrEmpty(ownerId)) { // TODO Koen: add a check on how we are logged on and if we can call the /me endpoint using that (i.e. won't work under appcreds) - + // Still no owner identified, see if we can make the current user executing this cmdlet the owner var user = await GraphHelper.GetAsync(httpClient, "v1.0/me?$select=Id", accessToken); @@ -273,6 +273,8 @@ private static async Task CreateGroupAsync(string accessToken, HttpClient GroupTypes = new List() { "Unified" }, SecurityEnabled = false, Owners = new List() { $"https://{PnPConnection.Current.GraphEndPoint}/v1.0/users/{ownerId}" }, + + // TODO Koen: check why the owners were also added as members, is this necessary? //group.Members = new List() { $"https://{PnPConnection.Current.GraphEndPoint}/v1.0/users/{ownerId}" }; Visibility = visibility == GroupVisibility.NotSpecified ? GroupVisibility.Private : visibility }; From 044b1a3f4fd411b0412b224e64630371ab3f41f0 Mon Sep 17 00:00:00 2001 From: Koen Zomers Date: Wed, 22 Dec 2021 23:02:02 +0100 Subject: [PATCH 17/24] Added some code comments --- src/Commands/Utilities/TeamsUtility.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Commands/Utilities/TeamsUtility.cs b/src/Commands/Utilities/TeamsUtility.cs index 3f011ea1d..a8a2dce3b 100644 --- a/src/Commands/Utilities/TeamsUtility.cs +++ b/src/Commands/Utilities/TeamsUtility.cs @@ -252,9 +252,11 @@ private static async Task CreateGroupAsync(string accessToken, HttpClient 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) @@ -265,6 +267,7 @@ private static async Task CreateGroupAsync(string accessToken, HttpClient } } + // Construct the new group Group group = new Group { DisplayName = displayName, @@ -277,10 +280,11 @@ private static async Task CreateGroupAsync(string accessToken, HttpClient 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}" }; + //group.Members = new List() { $"https://{PnPConnection.Current.GraphEndPoint}/v1.0/users/{ownerId}" }; } if (resourceBehaviorOptions != null && resourceBehaviorOptions.Length > 0) From a33a68055f7fe812de84a947097aa8ee1b5dcd3d Mon Sep 17 00:00:00 2001 From: Koen Zomers Date: Wed, 22 Dec 2021 23:11:06 +0100 Subject: [PATCH 18/24] Changed adding additional members and owners to add them by UPN in 1 request only --- src/Commands/Utilities/TeamsUtility.cs | 43 ++++++++++---------------- 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/src/Commands/Utilities/TeamsUtility.cs b/src/Commands/Utilities/TeamsUtility.cs index a8a2dce3b..515e93060 100644 --- a/src/Commands/Utilities/TeamsUtility.cs +++ b/src/Commands/Utilities/TeamsUtility.cs @@ -112,7 +112,8 @@ public static async Task NewTeamAsync(string accessToken, HttpClient httpC { Group group = null; Team returnTeam = null; - // Create group + + // Create the Group if (string.IsNullOrEmpty(groupId)) { group = await CreateGroupAsync(accessToken, httpClient, displayName, description, classification, mailNickname, visibility, owners, templateType, resourceBehaviorOptions); @@ -182,43 +183,31 @@ public static async Task NewTeamAsync(string accessToken, HttpClient httpC } } + // Construct a list of all owners and members to add + var teamOwnersAndMembers = new List(); + if (owners != null && owners.Length > 0) { - var chunks = BatchUtility.Chunk(owners, 20); - foreach (var chunk in chunks) + foreach (var owner in owners) { - var teamMembers = new List(); - var results = await BatchUtility.GetPropertyBatchedAsync(httpClient, accessToken, chunk.ToArray(), "/users/{0}", "id"); - - foreach (var userid in results.Select(r => r.Value)) - { - teamMembers.Add(new TeamChannelMember { Roles = new List { "owner" }, UserIdentifier = $"https://{PnPConnection.Current.GraphEndPoint}/v1.0/users('{userid}')" }); - } - if (teamMembers.Count > 0) - { - await GraphHelper.PostAsync(httpClient, $"v1.0/teams/{group.Id}/members/add", new { values = teamMembers }, accessToken); - } + teamOwnersAndMembers.Add(new TeamChannelMember { Roles = new List { "owner" }, UserIdentifier = $"https://{PnPConnection.Current.GraphEndPoint}/v1.0/users('{owner}')" }); } } if (members != null && members.Length > 0) { - var chunks = BatchUtility.Chunk(members, 20); - foreach (var chunk in chunks) + foreach (var member in members) { - var teamMembers = new List(); - var results = await BatchUtility.GetPropertyBatchedAsync(httpClient, accessToken, chunk.ToArray(), "/users/{0}", "id"); - - foreach (var userid in results.Select(r => r.Value)) - { - teamMembers.Add(new TeamChannelMember { Roles = new List { "member" }, UserIdentifier = $"https://{PnPConnection.Current.GraphEndPoint}/v1.0/users('{userid}')" }); - } - if (teamMembers.Count > 0) - { - await GraphHelper.PostAsync(httpClient, $"v1.0/teams/{group.Id}/members/add", new { values = teamMembers }, accessToken); - } + teamOwnersAndMembers.Add(new TeamChannelMember { Roles = new List(), UserIdentifier = $"https://{PnPConnection.Current.GraphEndPoint}/v1.0/users('{member}')" }); } } + + // If there are owners or members to add, execute the request to add them + if (teamOwnersAndMembers.Count > 0) + { + await GraphHelper.PostAsync(httpClient, $"v1.0/teams/{group.Id}/members/add", new { values = teamOwnersAndMembers }, accessToken); + } + } return returnTeam; } From 7e1c2915589c857a452ff43bf8554e71b4d849ed Mon Sep 17 00:00:00 2001 From: Koen Zomers Date: Wed, 22 Dec 2021 23:13:03 +0100 Subject: [PATCH 19/24] Removing commented out line to add the first owner as an owner and a member --- src/Commands/Utilities/TeamsUtility.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Commands/Utilities/TeamsUtility.cs b/src/Commands/Utilities/TeamsUtility.cs index 515e93060..d4326d3f7 100644 --- a/src/Commands/Utilities/TeamsUtility.cs +++ b/src/Commands/Utilities/TeamsUtility.cs @@ -273,7 +273,6 @@ private static async Task CreateGroupAsync(string accessToken, HttpClient 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) From 55e532a0560c834f900a9f961fd9c7ff9d72b8a4 Mon Sep 17 00:00:00 2001 From: Gautam Sheth Date: Sun, 26 Dec 2021 17:59:58 +0200 Subject: [PATCH 20/24] Split Owners and Members in array of 20 and then add them to the team --- src/Commands/Utilities/TeamsUtility.cs | 41 ++++++++++++++++---------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/src/Commands/Utilities/TeamsUtility.cs b/src/Commands/Utilities/TeamsUtility.cs index d4326d3f7..1e4e6275a 100644 --- a/src/Commands/Utilities/TeamsUtility.cs +++ b/src/Commands/Utilities/TeamsUtility.cs @@ -112,7 +112,7 @@ public static async Task NewTeamAsync(string accessToken, HttpClient httpC { Group group = null; Team returnTeam = null; - + // Create the Group if (string.IsNullOrEmpty(groupId)) { @@ -183,31 +183,39 @@ public static async Task NewTeamAsync(string accessToken, HttpClient httpC } } - // 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) + var chunkedOwners = BatchUtility.Chunk(owners, 20); + foreach (var chunk in chunkedOwners) { - teamOwnersAndMembers.Add(new TeamChannelMember { Roles = new List { "owner" }, UserIdentifier = $"https://{PnPConnection.Current.GraphEndPoint}/v1.0/users('{owner}')" }); + var teamOwners = new List(); + foreach (var owner in chunk) + { + teamOwners.Add(new TeamChannelMember { Roles = new List { "owner" }, UserIdentifier = $"https://{PnPConnection.Current.GraphEndPoint}/v1.0/users('{owner}')" }); + } + if (teamOwners.Count > 0) + { + await GraphHelper.PostAsync(httpClient, $"v1.0/teams/{group.Id}/members/add", new { values = teamOwners }, accessToken); + } } } if (members != null && members.Length > 0) { - foreach (var member in members) + var chunkedMembers = BatchUtility.Chunk(members, 20); + foreach (var chunk in chunkedMembers) { - teamOwnersAndMembers.Add(new TeamChannelMember { Roles = new List(), UserIdentifier = $"https://{PnPConnection.Current.GraphEndPoint}/v1.0/users('{member}')" }); + var teamMembers = new List(); + foreach (var member in chunk) + { + teamMembers.Add(new TeamChannelMember { Roles = new List(), UserIdentifier = $"https://{PnPConnection.Current.GraphEndPoint}/v1.0/users('{member}')" }); + } + if (teamMembers.Count > 0) + { + await GraphHelper.PostAsync(httpClient, $"v1.0/teams/{group.Id}/members/add", new { values = teamMembers }, accessToken); + } } } - - // If there are owners or members to add, execute the request to add them - if (teamOwnersAndMembers.Count > 0) - { - await GraphHelper.PostAsync(httpClient, $"v1.0/teams/{group.Id}/members/add", new { values = teamOwnersAndMembers }, accessToken); - } - } return returnTeam; } @@ -241,7 +249,7 @@ private static async Task CreateGroupAsync(string accessToken, HttpClient 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) { @@ -273,6 +281,7 @@ private static async Task CreateGroupAsync(string accessToken, HttpClient 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) From 093583c43f10f363523420d85d9eef3b3510d5e6 Mon Sep 17 00:00:00 2001 From: Gautam Sheth Date: Sat, 15 Jan 2022 19:16:11 +0200 Subject: [PATCH 21/24] Fix changelog entry issue --- CHANGELOG.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6145d41a..6b8016798 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +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 support to add multiple owners and members in `New-PnPTeamsTeam` cmdlet. ### Changed @@ -18,17 +20,24 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). - Fixed `Set-PnPSite` not working with `DisableCompanyWideSharingLinks` parameter. - Fixed `Get-PnPListPermissions` returing wrong information in case of broken inheritance. - Fixed `Submit-PnPSearchQuery -Query "somequery"` yielding an error when no results [#1520](https://github.com/pnp/powershell/pull/1520) +- Fixed `Set-PnPTenantSite` not setting SharingCapability property correctly. +- Fixed `Get-PnPMicrosoft365Group` retrieving non-Unified groups when parameters are not specified. + ### Removed ### Contributors +- Leon Armston [LeonArmston] +- Reshmee Auckloo [reshmee011] ## [1.9.0] ### Added - Added `Get-PnPTenantInstance` which will return one or more tenant instances, depending if you have a multi-geo or single-geo (default) tenant. +- Added optional `-ScheduledPublishDate` parameter to `Add-PnPPage` and `Set-PnPPage` to allow for scheduling a page to be published +- Added `-RemoveScheduledPublish` to `Set-PnPPage` to allow for a page publish schedule to be removed - Added support for off peak SharePoint Syntex content classification and extraction for lists and folders via new `-OffPeak` and `-Folder` parameters for `Request-PnPSyntexClassifyAndExtract` - Added `Get\Set-PnPPlannerConfiguration` to allow working with the Microsoft Planner tenant configuration - Added `Get\Set-PnPPlannerUserPolicy` to allow setting Microsoft Planner user policies for specific users @@ -57,9 +66,6 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). - Added optional `-IsDefault` option to `Get-PnPPowerPlatformEnvironment` which allows just the default or non default environments to be returned. If not provided, all environments will be returned as was the case before this addition. - Added `ResourceBehaviorOptions` option in `New-PnPTeamsTeam` cmdlet to set `ResourceBehaviorOptions` while provisioning a Team - Added alias on `Copy-PnPFile` for `Copy-PnPFolder`. It could already be used to copy a folder, but to make this more clear, and as we already had a `Copy/Move-PnPFolder` as well, the same cmdlet is now also available under its alternative cmdlet name. -- Added optional `-ScheduledPublishDate` parameter to `Add-PnPPage` and `Set-PnPPage` to allow for scheduling a page to be published. -- Added `-RemoveScheduledPublish` to `Set-PnPPage` to allow for a page publish schedule to be removed. -- Added support to add multiple owners and members in `New-PnPTeamsTeam` cmdlet. - Added `IsFluidEnabled` state to be returned with `Get-PnPTenant` cmdlet. - 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) @@ -665,4 +671,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 From cf4f0f28d04b554247a06989b08b61abee01794c Mon Sep 17 00:00:00 2001 From: Koen Zomers Date: Wed, 19 Jan 2022 12:07:12 +0100 Subject: [PATCH 22/24] Upping batches to 200 based on developer feedback --- src/Commands/Utilities/TeamsUtility.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Commands/Utilities/TeamsUtility.cs b/src/Commands/Utilities/TeamsUtility.cs index 1e4e6275a..b57d0b07f 100644 --- a/src/Commands/Utilities/TeamsUtility.cs +++ b/src/Commands/Utilities/TeamsUtility.cs @@ -185,7 +185,7 @@ public static async Task NewTeamAsync(string accessToken, HttpClient httpC if (owners != null && owners.Length > 0) { - var chunkedOwners = BatchUtility.Chunk(owners, 20); + var chunkedOwners = BatchUtility.Chunk(owners, 200); foreach (var chunk in chunkedOwners) { var teamOwners = new List(); @@ -202,7 +202,7 @@ public static async Task NewTeamAsync(string accessToken, HttpClient httpC if (members != null && members.Length > 0) { - var chunkedMembers = BatchUtility.Chunk(members, 20); + var chunkedMembers = BatchUtility.Chunk(members, 200); foreach (var chunk in chunkedMembers) { var teamMembers = new List(); From 806f62b26967ceb10f5016c445d7a4a7dc215600 Mon Sep 17 00:00:00 2001 From: Gautam Sheth Date: Sat, 22 Jan 2022 18:51:28 +0200 Subject: [PATCH 23/24] Updated as per discussed --- src/Commands/Utilities/TeamsUtility.cs | 38 +++++++++++--------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/src/Commands/Utilities/TeamsUtility.cs b/src/Commands/Utilities/TeamsUtility.cs index b57d0b07f..d2f6d9f01 100644 --- a/src/Commands/Utilities/TeamsUtility.cs +++ b/src/Commands/Utilities/TeamsUtility.cs @@ -183,39 +183,33 @@ public static async Task NewTeamAsync(string accessToken, HttpClient httpC } } + // Construct a list of all owners and members to add + var teamOwnersAndMembers = new List(); if (owners != null && owners.Length > 0) { - var chunkedOwners = BatchUtility.Chunk(owners, 200); - foreach (var chunk in chunkedOwners) + foreach (var owner in owners) { - var teamOwners = new List(); - foreach (var owner in chunk) - { - teamOwners.Add(new TeamChannelMember { Roles = new List { "owner" }, UserIdentifier = $"https://{PnPConnection.Current.GraphEndPoint}/v1.0/users('{owner}')" }); - } - if (teamOwners.Count > 0) - { - await GraphHelper.PostAsync(httpClient, $"v1.0/teams/{group.Id}/members/add", new { values = teamOwners }, accessToken); - } + teamOwnersAndMembers.Add(new TeamChannelMember { Roles = new List { "owner" }, UserIdentifier = $"https://{PnPConnection.Current.GraphEndPoint}/v1.0/users('{owner}')" }); } } if (members != null && members.Length > 0) { - var chunkedMembers = BatchUtility.Chunk(members, 200); - foreach (var chunk in chunkedMembers) + foreach (var member in members) { - var teamMembers = new List(); - foreach (var member in chunk) - { - teamMembers.Add(new TeamChannelMember { Roles = new List(), UserIdentifier = $"https://{PnPConnection.Current.GraphEndPoint}/v1.0/users('{member}')" }); - } - if (teamMembers.Count > 0) - { - await GraphHelper.PostAsync(httpClient, $"v1.0/teams/{group.Id}/members/add", new { values = teamMembers }, accessToken); - } + 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.ToList()) + { + await GraphHelper.PostAsync(httpClient, $"v1.0/teams/{group.Id}/members/add", new { values = chunk.ToList() }, accessToken); } } + } return returnTeam; } From 950cda225a5e7045bd1b7cdda9a4d10eacb53bc1 Mon Sep 17 00:00:00 2001 From: Koen Zomers Date: Wed, 26 Jan 2022 16:32:35 +0100 Subject: [PATCH 24/24] Removing redundant ToList --- src/Commands/Utilities/TeamsUtility.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Commands/Utilities/TeamsUtility.cs b/src/Commands/Utilities/TeamsUtility.cs index d2f6d9f01..03418591a 100644 --- a/src/Commands/Utilities/TeamsUtility.cs +++ b/src/Commands/Utilities/TeamsUtility.cs @@ -204,7 +204,7 @@ public static async Task NewTeamAsync(string accessToken, HttpClient httpC if (teamOwnersAndMembers.Count > 0) { var ownersAndMembers = BatchUtility.Chunk(teamOwnersAndMembers, 200); - foreach (var chunk in ownersAndMembers.ToList()) + foreach (var chunk in ownersAndMembers) { await GraphHelper.PostAsync(httpClient, $"v1.0/teams/{group.Id}/members/add", new { values = chunk.ToList() }, accessToken); }