diff --git a/docs/help/Get-PartnerUserSignInActivity.md b/docs/help/Get-PartnerUserSignInActivity.md
index 8aeec4b..dda7275 100644
--- a/docs/help/Get-PartnerUserSignInActivity.md
+++ b/docs/help/Get-PartnerUserSignInActivity.md
@@ -26,53 +26,39 @@ Gets the sign-in activities for the specified user.
### Example 1
```powershell
-PS C:\> Get-PartnerUserSignInActivity -UserId '3dd89389-b34c-4f5a-975d-516df5694d7e'
-```
-
-Gets the sign-in activities for the specified user.
-
-### Example 2
-```powershell
PS C:\> Get-PartnerUserSignInActivity -StartDate (Get-Date).AddDays(-7) -UserId '3dd89389-b34c-4f5a-975d-516df5694d7e'
```
Gets the sign-in activities from the past seven days for the specified user.
-### Example 3
+### Example 2
```powershell
-PS C:\> $users = Get-PartnerUser
-PS C:\> $activities = $users.ForEach({Get-PartnerUserSignInActivity -StartDate (Get-Date).AddDays(-7) -UserId $_.Id})
-PS C:\> $activities | ? {$_.AuthenticationDetails | ? {$_.Succeeded -eq $true}}
+PS C:\> Get-PartnerUserSignInActivity -StartDate (Get-Date).AddDays(-7) -UserId '3dd89389-b34c-4f5a-975d-516df5694d7e' | ? {$_.AuthenticationDetails | ? {$_.Succeeded -eq $true}}
```
-Gets the sign-in activities from the past seven days that have successfully authenticated.
+Gets the successful sign-in activities from the past seven days for the specified user.
+
+### Example 3
+```powershell
+PS C:\> Get-PartnerUserSignInActivity -StartDate (Get-Date).AddDays(-7) -UserId '3dd89389-b34c-4f5a-975d-516df5694d7e' | ? {$_.AuthenticationDetails | ? {$_.Succeeded -eq $true}} | ? {$_.MfaDetail -eq $null}
+```
+
+Gets the successful sign-in activities from the past seven days for the specified user that were not challenged by multi-factor authentication.
### Example 4
```powershell
-PS C:> $users = Get-PartnerUser
-PS C:> $activities = $users.ForEach({Get-PartnerUserSignInActivity -EndDate (Get-Date) -StartDate (Get-Date).AddDays(-7) -UserId $_.Id})
-PS C:> $activities | ? {$_.AuthenticationDetails | ? {$_.Succeeded -eq $true}} | ? {$_.ResourceId -eq 'fa3d9a0c-3fb0-42cc-9193-47c7ecd2edbd'}
+PS C:\> $signIns = Get-PartnerUserSignInActivity -StartDate (Get-Date).AddDays(-7) | ? {$_.AuthenticationDetails | ? {$_.Succeeded -eq $true}} | ? {$_.MfaDetail -eq $null}
```
-Gets the sign-in activities from the past seven days where the resource being accessed was the Partner Center API.
+Gets the successful sign-in activities all users from the past seven days in your partner tenant that were not challenged by multi-factor authentication.
### Example 5
```powershell
-PS C:\> $users = Get-PartnerUser
-PS C:\> $activities = $users.ForEach({Get-PartnerUserSignInActivity -StartDate (Get-Date).AddDays(-7) -UserId $_.Id})
-PS C:\> $activities | ? {$_.AuthenticationDetails | ? {$_.Succeeded -eq $true}} | ? {$_.MfaDetail -eq $null}
+PS C:\> $signIns = Get-PartnerUserSignInActivity -StartDate (Get-Date).AddDays(-7)
+PS C:\> $signIns | ? {$_.AuthenticationDetails | ? {$_.Succeeded -eq $true}} | ? {$_.MfaDetail -eq $null} | ? {$_.ResourceId -eq 'fa3d9a0c-3fb0-42cc-9193-47c7ecd2edbd'}
```
-Gets the sign-in activities from the past seven days that have successfully authenticated, but have not utilized multi-factor authentication.
-
-### Example 6
-```powershell
-PS C:> $users = Get-PartnerUser
-PS C:> $activities = $users.ForEach({Get-PartnerUserSignInActivity -EndDate (Get-Date) -StartDate (Get-Date).AddDays(-7) -UserId $_.Id})
-PS C:> $activities | ? {$_.AuthenticationDetails | ? {$_.Succeeded -eq $true}} | ? {$_.MfaDetail -eq $null} | ? {$_.ResourceId -eq 'fa3d9a0c-3fb0-42cc-9193-47c7ecd2edbd'}
-```
-
-Gets the sign-in activities from the past seven days where the resource being accessed was the Partner Center API and the sign-in activity was not challenged for multi-factor authentication.
+Gets the successful sign-in activities from the past seven days, where the resource being assessed was the Partner Center API and were not challenged by multi-factor authentication.
## PARAMETERS
diff --git a/src/PowerShell/Commands/GetPartnerUserSignInActivity.cs b/src/PowerShell/Commands/GetPartnerUserSignInActivity.cs
index bd7cf49..5267712 100644
--- a/src/PowerShell/Commands/GetPartnerUserSignInActivity.cs
+++ b/src/PowerShell/Commands/GetPartnerUserSignInActivity.cs
@@ -4,13 +4,16 @@
namespace Microsoft.Store.PartnerCenter.PowerShell.Commands
{
using System;
+ using System.Collections.Generic;
+ using System.Linq;
using System.Management.Automation;
+ using System.Net;
+ using System.Net.Http;
using System.Text.RegularExpressions;
+ using System.Threading.Tasks;
using Graph;
using Models.Authentication;
- using System.Collections.Generic;
using Network;
- using System.Threading.Tasks;
[Cmdlet(VerbsCommon.Get, "PartnerUserSignInActivity"), OutputType(typeof(SignIn))]
public class GetPartnerUserSignInActivity : PartnerAsyncCmdlet
@@ -20,6 +23,26 @@ namespace Microsoft.Store.PartnerCenter.PowerShell.Commands
///
private const string RequestFromNonPremiumTenant = "Authentication_RequestFromNonPremiumTenantOrB2CTenant";
+ ///
+ /// Represents the default amount of time used when calculating a random delta in the delay between retries.
+ ///
+ private static readonly TimeSpan DefaultClientBackoff = TimeSpan.FromSeconds(10.0);
+
+ ///
+ /// Represents the default maximum amount of time used when calculating the exponential delay between retries.
+ ///
+ private static readonly TimeSpan DefaultMaxBackoff = TimeSpan.FromSeconds(30.0);
+
+ ///
+ /// Represents the default minimum amount of time used when calculating the exponential delay between retries.
+ ///
+ private static readonly TimeSpan DefaultMinBackoff = TimeSpan.FromSeconds(1.0);
+
+ ///
+ /// The queue of paged Microsoft Graph request operations.
+ ///
+ private static readonly Queue PagedRequests = new Queue();
+
///
/// Gets or sets the end date portion of the query.
///
@@ -72,25 +95,89 @@ namespace Microsoft.Store.PartnerCenter.PowerShell.Commands
if (!string.IsNullOrEmpty(filter))
{
queryOptions = new List
- {
- new QueryOption("$filter", $"({filter})")
- };
+ {
+ new QueryOption("$filter", $"({filter})")
+ };
}
- collection = await client.AuditLogs.SignIns.Request(queryOptions).Top(500).GetAsync(CancellationToken).ConfigureAwait(false);
- signIns = new List(collection.CurrentPage);
-
- while (collection.NextPageRequest != null)
+ try
{
- collection = await collection.NextPageRequest.GetAsync(CancellationToken).ConfigureAwait(false);
- signIns.AddRange(collection.CurrentPage);
- }
+ collection = await client.AuditLogs.SignIns.Request(queryOptions).Top(500).GetAsync(CancellationToken).ConfigureAwait(false);
+ signIns = new List(collection.CurrentPage);
+ while (collection.NextPageRequest != null)
+ {
+ collection = await collection.NextPageRequest.GetAsync(CancellationToken).ConfigureAwait(false);
+ signIns.AddRange(collection.CurrentPage);
+ }
+ }
+ catch (ServiceException ex)
+ {
+ if (!ex.Error.Code.Equals(RequestFromNonPremiumTenant))
+ {
+ throw;
+ }
+
+ signIns = await GetSignInActivitiesAsync(client).ConfigureAwait(false);
+ }
WriteObject(signIns, true);
}, true);
}
+ private async Task> GetSignInActivitiesAsync(IGraphServiceClient client)
+ {
+ BatchRequestContent batchRequestContent;
+ BatchResponseContent batchResponseContent;
+ IGraphServiceUsersCollectionPage userCollection;
+ List signIns;
+ List users;
+ string filter = string.Empty;
+
+ userCollection = await client.Users.Request().Top(500).GetAsync().ConfigureAwait(false);
+ users = new List(userCollection.CurrentPage);
+
+ while (userCollection.NextPageRequest != null)
+ {
+ userCollection = await userCollection.NextPageRequest.GetAsync().ConfigureAwait(false);
+ users.AddRange(userCollection.CurrentPage);
+ }
+
+ if (StartDate != null)
+ {
+ filter = AppendValue(filter, $"createdDateTime ge {StartDate.Value.ToString("yyyy-MM-ddTHH:mm:ssZ")}");
+ }
+
+ if (EndDate != null)
+ {
+ filter = AppendValue(filter, $"createdDateTime le {EndDate.Value.ToString("yyyy-MM-ddTHH:mm:ssZ")}");
+ }
+
+ signIns = new List();
+
+ foreach (IEnumerable batch in Batch(users, 5))
+ {
+ batchRequestContent = new BatchRequestContent();
+
+ foreach (User user in batch)
+ {
+ batchRequestContent.AddBatchRequestStep(client.AuditLogs.SignIns.Request(new List
+ {
+ new QueryOption("$filter", $"({AppendValue(filter, $"userId eq '{user.Id}'")})")
+ }).Top(200));
+ }
+
+ batchResponseContent = await client.Batch.Request().PostAsync(batchRequestContent, CancellationToken).ConfigureAwait(false);
+ await ParseBatchResponseAsync(client, batchRequestContent, batchResponseContent, signIns).ConfigureAwait(false);
+ }
+
+ while (PagedRequests.Count != 0)
+ {
+ await ProcessPagedRequestsAsync(client, signIns).ConfigureAwait(false);
+ }
+
+ return signIns;
+ }
private static string AppendValue(string baseValue, string appendValue)
{
@@ -101,5 +188,114 @@ namespace Microsoft.Store.PartnerCenter.PowerShell.Commands
return $"{baseValue} and {appendValue}";
}
+
+ private static IEnumerable> Batch(IEnumerable entities, int batchSize)
+ {
+ int size = 0;
+
+ while (size < entities.Count())
+ {
+ yield return entities.Skip(size).Take(batchSize);
+ size += batchSize;
+ }
+ }
+
+ private async Task ParseBatchResponseAsync(IGraphServiceClient client, BatchRequestContent batchRequestContent, BatchResponseContent batchResponseContent, List signIns, int retryCount = 0)
+ {
+ AuditLogRootRestrictedSignInsCollectionResponse collection;
+ Dictionary responses;
+
+ client.AssertNotNull(nameof(client));
+ batchRequestContent.AssertNotNull(nameof(batchRequestContent));
+ batchResponseContent.AssertNotNull(nameof(batchResponseContent));
+ signIns.AssertNotNull(nameof(signIns));
+
+ responses = await batchResponseContent.GetResponsesAsync().ConfigureAwait(false);
+
+ foreach (KeyValuePair item in responses.Where(item => item.Value.IsSuccessStatusCode))
+ {
+ collection = await batchResponseContent
+ .GetResponseByIdAsync(item.Key).ConfigureAwait(false);
+
+ collection.AdditionalData.TryGetValue("@odata.nextLink", out object nextPageLink);
+ string nextPageLinkString = nextPageLink as string;
+
+ if (!string.IsNullOrEmpty(nextPageLinkString))
+ {
+ collection.Value.InitializeNextPageRequest(client, nextPageLinkString);
+ }
+
+ if (collection.Value.NextPageRequest != null)
+ {
+ PagedRequests.Enqueue(collection.Value.NextPageRequest);
+ }
+
+ signIns.AddRange(collection.Value);
+ }
+
+ if (PagedRequests.Count >= 5)
+ {
+ await ProcessPagedRequestsAsync(client, signIns).ConfigureAwait(false);
+ }
+
+ await RetryRequestWithTransientFaultAsync(client, batchRequestContent, responses, signIns, ++retryCount).ConfigureAwait(false);
+ }
+
+ private async Task ProcessPagedRequestsAsync(IGraphServiceClient client, List signIns)
+ {
+ BatchRequestContent batchRequestContent;
+ BatchResponseContent batchResponseContent;
+ int numberOfRequests;
+
+ client.AssertNotNull(nameof(client));
+ signIns.AssertNotNull(nameof(signIns));
+
+ if (PagedRequests.Count == 0)
+ {
+ return;
+ }
+
+ batchRequestContent = new BatchRequestContent();
+
+ numberOfRequests = PagedRequests.Count > 5 ? 5 : PagedRequests.Count;
+
+ for (int i = 0; i < numberOfRequests; i++)
+ {
+ batchRequestContent.AddBatchRequestStep(PagedRequests.Dequeue());
+ }
+
+ batchResponseContent = await client.Batch.Request().PostAsync(batchRequestContent, CancellationToken).ConfigureAwait(false);
+ await ParseBatchResponseAsync(client, batchRequestContent, batchResponseContent, signIns).ConfigureAwait(false);
+ }
+
+ private async Task RetryRequestWithTransientFaultAsync(IGraphServiceClient client, BatchRequestContent batchRequestContent, Dictionary responses, List signIns, int retryCount)
+ {
+ BatchRequestContent retryBatchRequestContent = new BatchRequestContent();
+ BatchResponseContent batchResponseContent;
+ Random random;
+ double delta;
+
+ client.AssertNotNull(nameof(client));
+ batchRequestContent.AssertNotNull(nameof(batchRequestContent));
+ responses.AssertNotNull(nameof(responses));
+
+ if (retryCount <= 3 && responses.Where(item => item.Value.StatusCode == (HttpStatusCode)429).Any())
+ {
+ foreach (KeyValuePair item in responses.Where(item => item.Value.StatusCode == (HttpStatusCode)429))
+ {
+ retryBatchRequestContent.AddBatchRequestStep(batchRequestContent.BatchRequestSteps[item.Key]);
+ }
+
+ random = new Random();
+ delta = (Math.Pow(2.0, retryCount) - 1.0) *
+ random.Next((int)(DefaultClientBackoff.TotalMilliseconds * 0.8), (int)(DefaultClientBackoff.TotalMilliseconds * 1.2));
+
+ await Task.Delay((int)Math.Min(DefaultMinBackoff.TotalMilliseconds + delta, DefaultMaxBackoff.TotalMilliseconds), CancellationToken).ConfigureAwait(false);
+
+ batchResponseContent = await client.Batch.Request().PostAsync(retryBatchRequestContent, CancellationToken).ConfigureAwait(false);
+
+ await ParseBatchResponseAsync(client, retryBatchRequestContent, batchResponseContent, signIns, retryCount).ConfigureAwait(false);
+ }
+ }
}
}
\ No newline at end of file