Esempio n. 1
0
        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);
        }
Esempio n. 2
0
        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);
        }
Esempio n. 4
0
        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);
        }
Esempio n. 5
0
        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);
        }
Esempio n. 6
0
        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;
                }
            }
        }