Skip to content

Commit

Permalink
Merge pull request #1241 from gautamdsheth/feature/1021
Browse files Browse the repository at this point in the history
#1021 - New-PnPTeamsTeam , ability to add multiple owners and members
  • Loading branch information
KoenZomers authored Jan 26, 2022
2 parents 99407aa + 9b84f5b commit dda26eb
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 55 deletions.
7 changes: 5 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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]
40 changes: 37 additions & 3 deletions documentation/New-PnPTeamsTeam.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ New-PnPTeamsTeam -GroupId <String> [-Owner <String>] [-AllowAddRemoveApps <Boole
[-AllowOwnerDeleteMessages <Boolean>] [-AllowStickersAndMemes <Boolean>] [-AllowTeamMentions <Boolean>]
[-AllowUserDeleteMessages <Boolean>] [-AllowUserEditMessages <Boolean>]
[-GiphyContentRating <TeamGiphyContentRating>] [-ShowInTeamsSearchAndSuggestions <Boolean>]
[-Classification <String>] [<CommonParameters>]
[-Classification <String>] [-Owners <String[]>] [-Members <String[]>] [<CommonParameters>]
```

### For a new group
Expand All @@ -42,7 +42,8 @@ New-PnPTeamsTeam -DisplayName <String> [-MailNickName <String>] [-Description <S
[-AllowOwnerDeleteMessages <Boolean>] [-AllowStickersAndMemes <Boolean>] [-AllowTeamMentions <Boolean>]
[-AllowUserDeleteMessages <Boolean>] [-AllowUserEditMessages <Boolean>]
[-GiphyContentRating <TeamGiphyContentRating>] [-Visibility <TeamVisibility>]
[-ShowInTeamsSearchAndSuggestions <Boolean>] [-Classification <String>]
[-ShowInTeamsSearchAndSuggestions <Boolean>] [-Classification <String>]
[-Owners <String[]>] [-Members <String[]>]
[-ResourceBehaviorOptions <TeamResourceBehaviorOptions>]
[<CommonParameters>]
```
Expand Down Expand Up @@ -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 "[email protected]","[email protected]" -Members "[email protected]"
```

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
Expand Down Expand Up @@ -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.
Expand All @@ -435,7 +470,6 @@ Accept pipeline input: False
Accept wildcard characters: False
```
## RELATED LINKS
[Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp)
Expand Down
21 changes: 19 additions & 2 deletions src/Commands/Teams/NewTeamsTeam.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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());
}
}
}
147 changes: 99 additions & 48 deletions src/Commands/Utilities/TeamsUtility.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
{
Expand Down Expand Up @@ -104,14 +108,15 @@ private static async Task<Team> ParseTeamJsonAsync(string accessToken, HttpClien
}
}

public static async Task<Team> 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<Team> 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)
Expand Down Expand Up @@ -161,7 +166,7 @@ public static async Task<Team> 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;
}
Expand All @@ -177,51 +182,102 @@ public static async Task<Team> NewTeamAsync(string accessToken, HttpClient httpC
retry = false;
}
}

// Construct a list of all owners and members to add
var teamOwnersAndMembers = new List<TeamChannelMember>();
if (owners != null && owners.Length > 0)
{
foreach (var owner in owners)
{
teamOwnersAndMembers.Add(new TeamChannelMember { Roles = new List<string> { "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<string>(), 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<Group> 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<Group> 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<User>(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<User>(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<User>(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<User>(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<User>(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<User>(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<string>() { "Unified" };
group.SecurityEnabled = false;
group.Owners = new List<string>() { $"https://{PnPConnection.Current.GraphEndPoint}/v1.0/users/{ownerId}" };
group.Members = new List<string>() { $"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<string>() { "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<string>() { $"https://{PnPConnection.Current.GraphEndPoint}/v1.0/users/{ownerId}" };
group.Members = new List<string>() { $"https://{PnPConnection.Current.GraphEndPoint}/v1.0/users/{ownerId}" };
}

if (resourceBehaviorOptions != null && resourceBehaviorOptions.Length > 0)
{
var teamResourceBehaviorOptionsValue = new List<string>();
Expand All @@ -235,22 +291,18 @@ private static async Task<Group> CreateGroupAsync(string accessToken, HttpClient
switch (templateType)
{
case TeamsTemplateType.EDU_Class:
{
group.Visibility = GroupVisibility.HiddenMembership;
group.CreationOptions = new List<string> { "ExchangeProvisioningFlags:461", "classAssignments" };
group.EducationObjectType = "Section";
break;
}
group.Visibility = GroupVisibility.HiddenMembership;
group.CreationOptions = new List<string> { "ExchangeProvisioningFlags:461", "classAssignments" };
group.EducationObjectType = "Section";
break;

case TeamsTemplateType.EDU_PLC:
{
group.CreationOptions = new List<string> { "PLC" };
break;
}
group.CreationOptions = new List<string> { "PLC" };
break;

default:
{
group.CreationOptions = new List<string> { "ExchangeProvisioningFlags:3552" };
break;
}
group.CreationOptions = new List<string> { "ExchangeProvisioningFlags:3552" };
break;
}
try
{
Expand All @@ -267,7 +319,6 @@ private static async Task<Group> CreateGroupAsync(string accessToken, HttpClient
throw;
}
}

}

private static async Task<string> CreateAliasAsync(HttpClient httpClient, string accessToken)
Expand Down

0 comments on commit dda26eb

Please sign in to comment.