private async Task <(List <DriveItem> DeltaPage, bool IsLast)> GetDeltaPageAsync() { IDriveItemDeltaCollectionPage _currentDeltaPage; if (_lastDeltaPage == null) { _currentDeltaPage = await this.GraphClient.Me.Drive.Root.Delta().Request().GetAsync(); } else { _currentDeltaPage = await _lastDeltaPage.NextPageRequest.GetAsync(); } _lastDeltaPage = _currentDeltaPage; // if the last page was received if (_currentDeltaPage.NextPageRequest == null) { var deltaLink = _currentDeltaPage.AdditionalData[Constants.OdataInstanceAnnotations.DeltaLink].ToString(); _lastDeltaPage.InitializeNextPageRequest(this.GraphClient, deltaLink); return(_currentDeltaPage.ToList(), true); } return(_currentDeltaPage.ToList(), false); }
private Task ProcessDriveChangesAsync(ref string deltaLink, Func <DriveItem, bool> relevancePredicate, Func <IEnumerable <DriveItem>, Task> action, string resetLink) { var graph = graphClientContext.GraphClient; IDriveItemDeltaRequest deltaRequest = new DriveItemDeltaRequest(deltaLink, graph, null); IDriveItemDeltaCollectionPage delta = null; var preResult = new List <Task>(); do { try { delta = deltaRequest.GetAsync().Result; } catch (Exception e) when((e.InnerException as ServiceException)?.Error?.Code == "resyncRequired") { delta = new DriveItemDeltaRequest(resetLink, graph, null).GetAsync().Result; } var relevantItems = delta.Where(relevancePredicate); if (!relevantItems.Any()) { continue; } preResult.Add(action.Invoke(relevantItems)); } while ((deltaRequest = delta.NextPageRequest) != null); deltaLink = (string)delta.AdditionalData["@odata.deltaLink"]; return(Task.WhenAll(preResult)); }
/// <summary> /// Request the delta stream from OneDrive to find files that have changed between notifications for this account /// </summary> /// <param name="state">Our internal state information for the subscription we're processing.</param> /// <param name="client">Graph client for the attached user.</param> /// <param name="log">Tracewriter for debug output</param> /// <returns></returns> private static async Task <List <DriveItem> > FindChangedDriveItemsAsync(StoredSubscriptionState state, GraphServiceClient client, TraceWriter log) { string DefaultLatestDeltaUrl = idaMicrosoftGraphUrl + "/v1.0/drives/" + state.DriveId + "/root/delta?token=latest"; // We default to reading the "latest" state of the drive, so we don't have to process all the files in the drive // when a new subscription comes in. string deltaUrl = state?.LastDeltaToken ?? DefaultLatestDeltaUrl; List <DriveItem> changedDriveItems = new List <DriveItem>(); // Create an SDK request using the URL, instead of building up the request using the SDK IDriveItemDeltaRequest request = new DriveItemDeltaRequest(deltaUrl, client, null); // We max out at 50 requests of delta responses, just for demo purposes. const int MaxLoopCount = 50; for (int loopCount = 0; loopCount < MaxLoopCount && request != null; loopCount++) { log.Info($"Making request for '{state.SubscriptionId}' to '{deltaUrl}' "); // Get the next page of delta results IDriveItemDeltaCollectionPage deltaResponse = await request.GetAsync(); // Filter to the audio files we're interested in working with and add them to our list var changedFiles = (from f in deltaResponse where f.File != null && f.Name != null && (f.Name.EndsWith(acceptedAudioFileExtension) || f.Audio != null) && f.Deleted == null select f); changedDriveItems.AddRange(changedFiles); // Figure out how to proceed, whether we have more pages of changes to retrieve or not. if (null != deltaResponse.NextPageRequest) { request = deltaResponse.NextPageRequest; } else if (null != deltaResponse.AdditionalData["@odata.deltaLink"]) { string deltaLink = (string)deltaResponse.AdditionalData["@odata.deltaLink"]; log.Verbose($"All changes requested, nextDeltaUrl: {deltaLink}"); state.LastDeltaToken = deltaLink; return(changedDriveItems); } else { // Shouldn't get here, but just in case, we don't want to get stuck in a loop forever. request = null; } } // If we exit the For loop without returning, that means we read MaxLoopCount pages without finding a deltaToken log.Info($"Read through MaxLoopCount pages without finding an end. Too much data has changed and we're going to start over on the next notification."); state.LastDeltaToken = DefaultLatestDeltaUrl; return(changedDriveItems); }
public async Task <IDriveItemDeltaCollectionPage> GetListDelta(string driveId, string deltaRequestUrl) { IDriveItemDeltaCollectionPage changes = null; if (string.IsNullOrEmpty(deltaRequestUrl)) { if (string.IsNullOrEmpty(driveId)) { logger.LogError("GetListDelta: You must provide either a driveId or deltaRequestUrl"); return(null); } // New delta request changes = await graphClient.Drives[driveId].Root.Delta().Request().GetAsync(); } else { changes = new DriveItemDeltaCollectionPage(); changes.InitializeNextPageRequest(graphClient, deltaRequestUrl); changes = await changes.NextPageRequest.GetAsync(); } return(changes); }
internal static async Task ManageSubscription(SubscriptionActivity currentSubscriptionActivity, string oid, string tid, string upn, IConfiguration config, IMsalAccountActivityStore msalAccountActivityStore, GraphServiceClient _graphServiceClient, IMsalTokenCacheProvider msalTokenCacheProvider) { string subscriptionId = null; string changeToken = null; string notiticationUrl = config.GetValue <string>("Files:SubscriptionService"); int subscriptionLifeTimeInMinutes = config.GetValue <int>("Files:SubscriptionLifeTime"); if (subscriptionLifeTimeInMinutes == 0) { subscriptionLifeTimeInMinutes = 15; } // Load the current subscription (if any) var currentSubscriptions = await _graphServiceClient.Subscriptions.Request().GetAsync(); var currentOneDriveSubscription = currentSubscriptions.FirstOrDefault(p => p.Resource == "me/drive/root"); // If present and still using the same subscription host then update the subscription expiration date if (currentOneDriveSubscription != null && currentOneDriveSubscription.NotificationUrl.Equals(notiticationUrl, StringComparison.InvariantCultureIgnoreCase)) { // Extend the expiration of the current subscription subscriptionId = currentOneDriveSubscription.Id; currentOneDriveSubscription.ExpirationDateTime = DateTimeOffset.UtcNow.AddMinutes(subscriptionLifeTimeInMinutes); currentOneDriveSubscription.ClientState = Constants.FilesSubscriptionServiceClientState; await _graphServiceClient.Subscriptions[currentOneDriveSubscription.Id].Request().UpdateAsync(currentOneDriveSubscription); // Check if the last change token was populated if (currentSubscriptionActivity == null) { currentSubscriptionActivity = await msalAccountActivityStore.GetSubscriptionActivityForUserSubscription(oid, tid, upn, subscriptionId); } if (currentSubscriptionActivity != null) { changeToken = currentSubscriptionActivity.LastChangeToken; } } else { // Add a new subscription var newSubscription = await _graphServiceClient.Subscriptions.Request().AddAsync(new Subscription() { ChangeType = "updated", NotificationUrl = notiticationUrl, Resource = "me/drive/root", ExpirationDateTime = DateTimeOffset.UtcNow.AddMinutes(subscriptionLifeTimeInMinutes), ClientState = Constants.FilesSubscriptionServiceClientState, LatestSupportedTlsVersion = "v1_2" }); subscriptionId = newSubscription.Id; } // Store the user principal name with the subscriptionid as that's the mechanism needed to connect change event with tenant/user var cacheEntriesToRemove = await msalAccountActivityStore.UpdateSubscriptionId(subscriptionId, oid, tid, upn); // If we've found old MSAL cache entries for which we've removed the respective MsalActivity records we should also // drop these from the MSAL cache itself if (cacheEntriesToRemove.Any()) { foreach (var cacheEntry in cacheEntriesToRemove) { await(msalTokenCacheProvider as IntegratedTokenCacheAdapter).RemoveKeyFromCache(cacheEntry); } } if (changeToken == null) { // Initialize the subscription and get the latest change token, to avoid getting back all the historical changes IDriveItemDeltaCollectionPage deltaCollection = await _graphServiceClient.Me.Drive.Root.Delta("latest").Request().GetAsync(); var deltaLink = deltaCollection.AdditionalData["@odata.deltaLink"]; if (!string.IsNullOrEmpty(deltaLink.ToString())) { changeToken = ProcessChanges.GetChangeTokenFromUrl(deltaLink.ToString()); } } // Store a record per user/subscription to track future delta queries if (currentSubscriptionActivity == null) { currentSubscriptionActivity = new SubscriptionActivity(oid, tid, upn) { SubscriptionId = subscriptionId, LastChangeToken = changeToken }; } currentSubscriptionActivity.LastChangeToken = changeToken; currentSubscriptionActivity.SubscriptionId = subscriptionId; await msalAccountActivityStore.UpsertSubscriptionActivity(currentSubscriptionActivity); }
public async Task Run([QueueTrigger(Constants.OneDriveFileNotificationsQueue)] string myQueueItem, ILogger log) { log.LogInformation($"C# Queue trigger function processed: {myQueueItem}"); var notification = JsonSerializer.Deserialize <ChangeNotification>(myQueueItem); // Get the most recently updated msal account information for this subscription var account = await _msalAccountActivityStore.GetMsalAccountActivityForSubscription(notification.SubscriptionId); if (account != null) { // Configure the confidential client to get an access token for the needed resource var app = ConfidentialClientApplicationBuilder.Create(_config.GetValue <string>("AzureAd:ClientId")) .WithClientSecret(_config.GetValue <string>("AzureAd:ClientSecret")) .WithAuthority($"{_config.GetValue<string>("AzureAd:Instance")}{_config.GetValue<string>("AzureAd:TenantId")}") .Build(); // Initialize the MSAL cache for the specific account var msalCache = new BackgroundWorkerTokenCacheAdapter(account.AccountCacheKey, _serviceProvider.GetService <IDistributedCache>(), _serviceProvider.GetService <ILogger <MsalDistributedTokenCacheAdapter> >(), _serviceProvider.GetService <IOptions <MsalDistributedTokenCacheAdapterOptions> >()); await msalCache.InitializeAsync(app.UserTokenCache); // Prepare an MsalAccount instance representing the user we want to get a token for var hydratedAccount = new MsalAccount { HomeAccountId = new AccountId( account.AccountIdentifier, account.AccountObjectId, account.AccountTenantId) }; try { // Use the confidential MSAL client to get a token for the user we need to impersonate var result = await app.AcquireTokenSilent(Constants.BasePermissionScopes, hydratedAccount) .ExecuteAsync() .ConfigureAwait(false); //log.LogInformation($"Token acquired: {result.AccessToken}"); // Configure the Graph SDK to use an auth provider that takes the token we've just requested var authenticationProvider = new DelegateAuthenticationProvider( (requestMessage) => { requestMessage.Headers.Authorization = new AuthenticationHeaderValue(CoreConstants.Headers.Bearer, result.AccessToken); return(Task.FromResult(0)); }); var graphClient = new GraphServiceClient(authenticationProvider); // Retrieve the last used subscription activity information for this user+subscription var subscriptionActivity = await _msalAccountActivityStore.GetSubscriptionActivityForUserSubscription(account.AccountObjectId, account.AccountTenantId, account.UserPrincipalName, notification.SubscriptionId); // Make graph call on behalf of the user: do the delta query providing the last used change token so only new changes are returned IDriveItemDeltaCollectionPage deltaCollection = await graphClient.Me.Drive.Root.Delta(subscriptionActivity.LastChangeToken).Request().GetAsync(); bool morePagesAvailable = false; do { // If there is a NextPageRequest, there are more pages morePagesAvailable = deltaCollection.NextPageRequest != null; foreach (var driveItem in deltaCollection.CurrentPage) { await ProcessDriveItemChanges(driveItem, log); } if (morePagesAvailable) { // Get the next page of results deltaCollection = await deltaCollection.NextPageRequest.GetAsync(); } }while (morePagesAvailable); // Get the last used change token var deltaLink = deltaCollection.AdditionalData["@odata.deltaLink"]; if (!string.IsNullOrEmpty(deltaLink.ToString())) { var token = GetChangeTokenFromUrl(deltaLink.ToString()); subscriptionActivity.LastChangeToken = token; } // Persist back the last used change token await _msalAccountActivityStore.UpsertSubscriptionActivity(subscriptionActivity); } catch (MsalUiRequiredException ex) { /* * If MsalUiRequiredException is thrown for an account, it means that a user interaction is required * thus the background worker wont be able to acquire a token silently for it. * The user of that account will have to access the web app to perform this interaction. * Examples that could cause this: MFA requirement, token expired or revoked, token cache deleted, etc */ await _msalAccountActivityStore.HandleIntegratedTokenAcquisitionFailure(account); log.LogError($"Could not acquire token for account {account.UserPrincipalName}."); log.LogError($"Error: {ex.Message}"); } catch (Exception ex) { throw ex; } } }