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