Skip to content

Commit

Permalink
Merge pull request #2466 from koskila/feature/expiringgroups
Browse files Browse the repository at this point in the history
Add new commandlet, Get-PnPExpiringMicrosoft365Groups
  • Loading branch information
gautamdsheth authored Oct 15, 2022
2 parents 75acb7e + b0759af commit c555289
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 6 deletions.
65 changes: 65 additions & 0 deletions documentation/Get-PnPMicrosoft365ExpiringGroup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
Module Name: PnP.PowerShell
title: Get-PnPMicrosoft365ExpiringGroup
schema: 2.0.0
applicable: SharePoint Online
external help file: PnP.PowerShell.dll-Help.xml
online version: https://pnp.github.io/powershell/cmdlets/Get-PnPMicrosoft365ExpiringGroup.html
---

# Get-PnPMicrosoft365ExpiringGroup

## SYNOPSIS

**Required Permissions**

* Microsoft Graph API : One of Directory.Read.All, Directory.ReadWrite.All, Group.Read.All, Group.ReadWrite.All, GroupMember.Read.All, GroupMember.ReadWrite.All

Gets all expiring Microsoft 365 Groups.

## SYNTAX

```powershell
Get-PnPMicrosoft365ExpiringGroup [-Limit <Int32>] [<CommonParameters>]
```

## DESCRIPTION
This command returns all expiring Microsoft 365 Groups within certain time. By default, groups expiring in the next 31 days are return (in accordance with SharePoint/OneDrive's retention period's 31-day months).

## EXAMPLES

### EXAMPLE 1
```powershell
Get-PnPMicrosoft365ExpiringGroup
```

Returns all Groups expiring within 31 days (roughly 1 month).

### EXAMPLE 2
```powershell
Get-PnPMicrosoft365ExpiringGroup -Limit 93
```

Returns all Microsoft 365 Groups expiring in 93 days (roughly 3 months)


## PARAMETERS

### -Limit

Limits Groups to be returned to Groups expiring in as many days.

```yaml
Type: Int32
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)
31 changes: 31 additions & 0 deletions src/Commands/Microsoft365Groups/GetExpiringMicrosoft365Group.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using PnP.PowerShell.Commands.Attributes;
using PnP.PowerShell.Commands.Base;
using PnP.PowerShell.Commands.Base.PipeBinds;
using PnP.PowerShell.Commands.Utilities;
using System;
using System.Linq;
using System.Management.Automation;

namespace PnP.PowerShell.Commands.Microsoft365Groups
{
[Cmdlet(VerbsCommon.Get, "PnPMicrosoft365ExpiringGroup")]
[RequiredMinimalApiPermissions("Group.Read.All")]
public class GetExpiringMicrosoft365Group : PnPGraphCmdlet
{
[Parameter(Mandatory = false)]
public SwitchParameter IncludeSiteUrl;

[Parameter(Mandatory = false)]
public SwitchParameter IncludeOwners;

[Parameter(Mandatory = false)]
public int Limit = 31;

protected override void ExecuteCmdlet()
{
var expiringGroups = Microsoft365GroupsUtility.GetExpiringGroupAsync(Connection, AccessToken, Limit, IncludeSiteUrl, IncludeOwners).GetAwaiter().GetResult();

WriteObject(expiringGroups.OrderBy(p => p.DisplayName), true);
}
}
}
52 changes: 51 additions & 1 deletion src/Commands/Utilities/Microsoft365GroupsUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ internal static async Task<IEnumerable<Microsoft365Group>> GetGroupsAsync(PnPCon
}
return items;
}

internal static async Task<Microsoft365Group> GetGroupAsync(PnPConnection connection, Guid groupId, string accessToken, bool includeSiteUrl, bool includeOwners)
{
var results = await GraphHelper.GetAsync<RestResultCollection<Microsoft365Group>>(connection, $"v1.0/groups?$filter=groupTypes/any(c:c+eq+'Unified') and id eq '{groupId}'", accessToken);
Expand Down Expand Up @@ -101,6 +102,7 @@ internal static async Task<Microsoft365Group> GetGroupAsync(PnPConnection connec
}
return null;
}

internal static async Task<Microsoft365Group> GetGroupAsync(PnPConnection connection, string displayName, string accessToken, bool includeSiteUrl, bool includeOwners)
{
var results = await GraphHelper.GetAsync<RestResultCollection<Microsoft365Group>>(connection, $"v1.0/groups?$filter=groupTypes/any(c:c+eq+'Unified') and (displayName eq '{displayName}' or mailNickName eq '{displayName}')", accessToken);
Expand All @@ -125,6 +127,55 @@ internal static async Task<Microsoft365Group> GetGroupAsync(PnPConnection connec
return null;
}

internal static async Task<IEnumerable<Microsoft365Group>> GetExpiringGroupAsync(PnPConnection connection, string accessToken, int limit, bool includeSiteUrl, bool includeOwners)
{
var items = new List<Microsoft365Group>();

var dateLimit = DateTime.UtcNow;
var dateStr = dateLimit.AddDays(limit).ToString("yyyy-MM-ddTHH:mm:ssZ");

// This query requires ConsistencyLevel header to be set.
var additionalHeaders = new Dictionary<string, string>();
additionalHeaders.Add("ConsistencyLevel", "eventual");

// $count=true needs to be here for reasons
// see this for some additional details: https://learn.microsoft.com/en-us/graph/aad-advanced-queries?tabs=http#group-properties
var result = await GraphHelper.GetResultCollectionAsync<Microsoft365Group>(connection, $"v1.0/groups?$filter=groupTypes/any(c:c+eq+'Unified') and expirationDateTime le {dateStr}&$count=true", accessToken, additionalHeaders:additionalHeaders);
if (result != null && result.Any())
{
items.AddRange(result);
}
if (includeSiteUrl || includeOwners)
{
var chunks = BatchUtility.Chunk(items.Select(g => g.Id.ToString()), 20);
if (includeOwners)
{
foreach (var chunk in chunks)
{
var ownerResults = await BatchUtility.GetObjectCollectionBatchedAsync<Microsoft365User>(connection, accessToken, chunk.ToArray(), "/groups/{0}/owners");
foreach (var ownerResult in ownerResults)
{
items.First(i => i.Id.ToString() == ownerResult.Key).Owners = ownerResult.Value;
}
}
}

if (includeSiteUrl)
{
foreach (var chunk in chunks)
{
var results = await BatchUtility.GetPropertyBatchedAsync(connection, accessToken, chunk.ToArray(), "/groups/{0}/sites/root", "webUrl");
//var results = await GetSiteUrlBatchedAsync(connection, accessToken, chunk.ToArray());
foreach (var batchResult in results)
{
items.First(i => i.Id.ToString() == batchResult.Key).SiteUrl = batchResult.Value;
}
}
}
}
return items;
}

internal static async Task<Microsoft365Group> GetDeletedGroupAsync(PnPConnection connection, Guid groupId, string accessToken)
{
return await GraphHelper.GetAsync<Microsoft365Group>(connection, $"v1.0/directory/deleteditems/microsoft.graph.group/{groupId}", accessToken);
Expand Down Expand Up @@ -168,7 +219,6 @@ internal static async Task AddMembersAsync(PnPConnection connection, Guid groupI

internal static string GetUserGraphUrlForUPN(string upn)
{

var escapedUpn = upn.Replace("#", "%23");

if (escapedUpn.StartsWith("$")) return $"users('{escapedUpn}')";
Expand Down
10 changes: 5 additions & 5 deletions src/Commands/Utilities/REST/GraphHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,17 +92,17 @@ public static async Task<HttpResponseMessage> GetResponseAsync(PnPConnection con
/// <param name="camlCasePolicy">Policy indicating the CamlCase that should be applied when mapping results to typed objects</param>
/// <param name="propertyNameCaseInsensitive">Indicates if the response be mapped to the typed object ignoring different casing</param>
/// <returns>List with objects of type T returned by the request</returns>
public static async Task<IEnumerable<T>> GetResultCollectionAsync<T>(PnPConnection connection, string url, string accessToken, bool camlCasePolicy = true, bool propertyNameCaseInsensitive = false)
public static async Task<IEnumerable<T>> GetResultCollectionAsync<T>(PnPConnection connection, string url, string accessToken, bool camlCasePolicy = true, bool propertyNameCaseInsensitive = false, IDictionary<string, string> additionalHeaders = null)
{
var results = new List<T>();
var request = await GetAsync<RestResultCollection<T>>(connection, url, accessToken, camlCasePolicy, propertyNameCaseInsensitive);
var request = await GetAsync<RestResultCollection<T>>(connection, url, accessToken, camlCasePolicy, propertyNameCaseInsensitive, additionalHeaders);

if (request.Items.Any())
{
results.AddRange(request.Items);
while (!string.IsNullOrEmpty(request.NextLink))
{
request = await GetAsync<RestResultCollection<T>>(connection, request.NextLink, accessToken, camlCasePolicy, propertyNameCaseInsensitive);
request = await GetAsync<RestResultCollection<T>>(connection, request.NextLink, accessToken, camlCasePolicy, propertyNameCaseInsensitive, additionalHeaders);
if (request.Items.Any())
{
results.AddRange(request.Items);
Expand All @@ -123,9 +123,9 @@ public static async Task<IEnumerable<T>> GetResultCollectionAsync<T>(PnPConnecti
/// <param name="camlCasePolicy">Policy indicating the CamlCase that should be applied when mapping results to typed objects</param>
/// <param name="propertyNameCaseInsensitive">Indicates if the response be mapped to the typed object ignoring different casing</param>
/// <returns>List with objects of type T returned by the request</returns>
public static async Task<T> GetAsync<T>(PnPConnection connection, string url, string accessToken, bool camlCasePolicy = true, bool propertyNameCaseInsensitive = false)
public static async Task<T> GetAsync<T>(PnPConnection connection, string url, string accessToken, bool camlCasePolicy = true, bool propertyNameCaseInsensitive = false, IDictionary<string, string> additionalHeaders = null)
{
var stringContent = await GetAsync(connection, url, accessToken);
var stringContent = await GetAsync(connection, url, accessToken, additionalHeaders);
if (stringContent != null)
{
var options = new JsonSerializerOptions { IgnoreNullValues = true };
Expand Down

0 comments on commit c555289

Please sign in to comment.