Threading Issues with SearchExecutor.ExecuteQueries

I have written a custom search control that passes a user's search query to an ashx handler, which then performs the search and returns the results back to my control serialized as JSON. The handler executes multiple queries, so I'm using the SearchExecutor.ExecuteQueries method to perform the search. It appears that this method is losing the HttpContext information when there are multiple KeywordQueries being executed - in which case, I'm getting results returned for users that they do not have permission to access. If there is only one KeywordQuery in the dictionary that is passed to ExecuteQueries, then the results are returned correctly. So I'm assuming it's a threading issue.

Here is the code for my handler (with query information simplified):

public class SPSearchTest : IHttpHandler, System.Web.SessionState.IRequiresSessionState
    {       
        public bool IsReusable
        {
            get { return false; }
        }

        public void ProcessRequest(HttpContext context)
        {
            string masterQuery = "";
                
            if (context.Request.QueryString["Query"] != null)
                masterQuery = context.Request.QueryString["Query"];
                                            
            SPSite searchSite = SPContext.Current.Site;
                                            
            string jsonObj = "";         

            Dictionary<string, Query> allQueries = new Dictionary<string, Query>();

            KeywordQuery docQuery = new KeywordQuery(searchSite);
            docQuery.QueryText = masterQuery + " AND SPContentType=Document";
            allQueries.Add("Documents", docQuery);

            KeywordQuery eventQuery = new KeywordQuery(searchSite);
            eventQuery.QueryText = masterQuery + " AND SPContentType=Event";
            allQueries.Add("Events", eventQuery);

            SearchExecutor searchExecutor = new SearchExecutor();
            Dictionary<string, ResultTableCollection> resultTableCollection = searchExecutor.ExecuteQueries(allQueries);
            IEnumerable<ResultTable> resultTables = resultTableCollection["Documents"].Filter("TableType", KnownTableTypes.RelevantResults);
            ResultTable resultTable = resultTables.FirstOrDefault();
            DataTable results = resultTable.Table;

            jsonObj = JsonConvert.SerializeObject(results, Common.serializerSettings);
                                
            context.Response.ContentType = "text/plain";
            context.Response.Write(jsonObj);
        }
        
    }

If I remove the "Events" query, the results from the "Documents" query consists of 1 document, which is correct. But when the "Events" query is included, the "Documents" query returns 2 results, one of which the user does not have permission to access.

I can't find much out there on this ExecuteQueries method and how it handles the search threads. I was hoping to use it to avoid threading issues of my own, but it doesn't appear to be working.

May 30th, 2015 12:47am

Turn on verbose logging for Search queries in Central Admin. Check the ULS logs for "In SearchExecutor.ExecuteQueries, CurrentPrincipal=?, WindowsIdentity.GetCurrent = ? ...

That should give you some information whether the thread is losing its identity. If it is bringing back documents that the current user does not have access to then the identity must be elevating.

Free Windows Admin Tool Kit Click here and download it now
May 30th, 2015 5:54pm

Thank you Steve. I followed your suggestion, and indeed the logs show an incorrect user as the CurrentPrincipal for methods related to security trimming:

Microsoft.Ceres.InteractionEngine.Processing.BuiltIn.SecurityPreFilterProducer : CurrentPrincipal: 'WRONG USER'...

Microsoft.Office.Server.Search.Query.Ims.Security.SecurityTrimmerPostProducer : CurrentPrincipal: 'WRONG USER'...

And the 'WRONG USER' is a Farm Admin & the Search Service Account. If I execute the same search sequentially using ExecuteQuery the CurrentPrincipal is correct and the results are correctly security trimmed. In addition, if I pass ExecuteQueries a dictionary with just one query in it, the CurrentPrincipal value is correct.

I've tried a couple of alternate ways of writing the code to see if I could retain the context in the threads, for example instantiating a new SPSite using a SiteID and the correct user's UserToken and passing that SPSite in when creating each new KeywordQuery - instead of the SPContext.Current.Site - but none of my changes have worked.

This behavior of ExecuteQueries seems like a bug to me.

May 31st, 2015 3:34pm

Not sure it is a bug. According to the reflected code when there is more than one query then ExecuteQueryTask is called. This code checks Thread.CurrentPrincipal.Identity.IsAuthenticated. If IsAuthenticated is false then this method calls WindowsIdentity.Impersonate which basically reverts to the search account. Of course this account has read access to everything. I would check to see if users calling your handler are authenticated, or the handler is being called right after login and the asp.net pipeline has not been able to set this property yet.
Free Windows Admin Tool Kit Click here and download it now
May 31st, 2015 6:48pm

Thanks for the additional follow-up, Steve. It's definitely not happening right after login. Within the ProcessRequest method of the handler, I logged the values for Thread.CurrentPrincipal.Identity.Name and Thread.CurrentPrincipal.Identity.IsAuthenticated - the Name is the correct user and IsAuthenticated is set to true. 

But if each of those searches are being performed in a new thread, I don't see how the Thread.CurrentPrincipal of those threads would be set to the correct user unless there was a way to pass in the HttpContext or Thread.CurrentPrincipal into the ExecuteQueries method, which there isn't. Or am I wrong about that?

May 31st, 2015 10:42pm

Ok, here's a little more information:

I checked out the decompiled ExecuteQueryTask method and ran some debugging on the searches. It looks to me like the method is checking the correct Thread.CurrentPrincipal and that IsAuthenticated is returning true. Looking at the code, this would mean that the WindowsImpersonationContext is never set to the WindowsIdentity that is passed in to the method as a variable.

Because of that, it appears that the WindowsImpersonationContext is indeed reverting to the search service account. I don't know if that would make a difference to the search unless the SecurityTrimmer makes use of that context instead of the Thread.CurrentPrincipal - and I can't seem to find any of the security trimming code in reflector.

Free Windows Admin Tool Kit Click here and download it now
June 1st, 2015 11:51am

The context variable is only set so that the Finally can call Undo. The task will use the current WindowsIdentity otherwise. Check the logs for identity information when SeachExecutor.ExecuteQueryTask is called. I still think it is the authentication. What kind of authentication are you using? Are you going across domains? The code still is not right, because it should not revert just because the current identity is not authenticated. It should throw an error there since now you are searching as search service account.
June 1st, 2015 12:02pm

Windows Authentication and no, not going across domains.

I can see where the ExecuteQueryTask should be logging identity information, but I can't find it in my logs even though I've set the trace log level to VerboseEx for all the Search categories. Anyway, I will have to look into that...

It may very well be related to authentication, but the correct current identity (via Thread.CurrentPrincipal) is returning "true" for IsAuthenticated, so it's not reverting because of an authentication failure. I believe it's reverting because it's somehow losing the WindowsIdentity of the logged in user due to the new task starting up.

I can replicate this if I change my handler to execute multiple queries asynchronously via Task.Wait in the same way that ExecuteQueries does, as follows:

public class SPSearchTest : IHttpHandler, System.Web.SessionState.IRequiresSessionState
    {
        public bool IsReusable
        {
            get { return false; }
        }

        public void ProcessRequest(HttpContext context)
        {
            string masterQuery = "";

            if (context.Request.QueryString["searchString"] != null)
                masterQuery = context.Request.QueryString["searchString"];

            context.Response.Write(String.Format("Thread.CurrentPrincipal: {0}, Windows.Identity: {1}, HttpContext.CurrentUser: {2}", Thread.CurrentPrincipal.Identity.Name, WindowsIdentity.GetCurrent().Name, context.User.Identity.Name));

            SPSite searchSite = SPContext.Current.Site;

            string jsonObj = "";

            Dictionary<string, KeywordQuery> allQueries = new Dictionary<string, KeywordQuery>();

            KeywordQuery docQuery = new KeywordQuery(searchSite);
            docQuery.QueryText = masterQuery + " AND SPContentType=Document";
            allQueries.Add("Documents", docQuery);

            KeywordQuery eventQuery = new KeywordQuery(searchSite);
            eventQuery.QueryText = masterQuery + " AND SPContentType=Event";
            allQueries.Add("Events", eventQuery);

           Dictionary<string, Task<ResultTableCollection>> queryTasks = new Dictionary<string, Task<ResultTableCollection>>();

            foreach (KeyValuePair<string, KeywordQuery> kvp in allQueries)
            {
                           
                Task<ResultTableCollection> task = Task<ResultTableCollection>.Factory.StartNew(
                    () => DoSearch(context, WindowsIdentity.GetCurrent(), kvp.Key, kvp.Value)                    
                    );
                queryTasks.Add(kvp.Key, task);
            }

            Task.WaitAll(queryTasks.Values.ToArray<Task<ResultTableCollection>>());
            Dictionary<string,ResultTableCollection> resultTableCollection = new Dictionary<string, ResultTableCollection>();

            foreach (KeyValuePair<string, Task<ResultTableCollection>> pair in queryTasks)
            {
                Task<ResultTableCollection> task = pair.Value;
                if (task.Status != TaskStatus.Faulted)
                {
                    resultTableCollection.Add(pair.Key, task.Result);
                }
            }

            IEnumerable<ResultTable> resultTables = resultTableCollection["Documents"].Filter("TableType", KnownTableTypes.RelevantResults);
            ResultTable resultTable = resultTables.FirstOrDefault();
            DataTable results = resultTable.Table;

            jsonObj = JsonConvert.SerializeObject(results, Common.serializerSettings);

            context.Response.ContentType = "text/plain";
            context.Response.Write(jsonObj);
        }

        private ResultTableCollection DoSearch(HttpContext context, WindowsIdentity windowsIdentity, string scope, KeywordQuery query)
        {
            if(Thread.CurrentPrincipal != null)
                context.Response.Write(String.Format(" Scope is {0} and Thread.CurrentPrincipal is {1}. ", scope, Thread.CurrentPrincipal.Identity.Name));
            else 
                context.Response.Write(String.Format(" Scope is {0} and Thread.CurrentPrincipal is null. ", scope));
            
            if (WindowsIdentity.GetCurrent() != null)
                context.Response.Write(String.Format(" Scope is {0} and Windows.Identity is {1}. ", scope, WindowsIdentity.GetCurrent().Name));
            else
                context.Response.Write(String.Format(" Scope is {0} and Windows.Identity is null. ", scope));

            WindowsImpersonationContext wic = null;
            if (!Thread.CurrentPrincipal.Identity.IsAuthenticated)
            {
                context.Response.Write(String.Format(" Thread.CurrentPrincipal.Identity is not authenticated for {0} scope. ", scope));

                if (windowsIdentity != null)
                    wic = windowsIdentity.Impersonate();
                else
                    context.Response.Write(String.Format(" Scope is {0} and cannot set Windows.Identity because variable is null. ", scope));

            }
            else
                context.Response.Write(String.Format(" Thread.CurrentPrincipal.Identity IS authenticated for {0} scope. If it was not, the WIC would have been set to {1}. ", scope, windowsIdentity.Name));


            if (Thread.CurrentPrincipal != null)
                context.Response.Write(String.Format(" After doing stuff - Scope is {0} and Thread.CurrentPrincipal is {1}. ", scope, Thread.CurrentPrincipal.Identity.Name));
            else
                context.Response.Write(String.Format(" After doing stuff - Scope is {0} and Thread.CurrentPrincipal is null. ", scope));

            if (WindowsIdentity.GetCurrent() != null)
                context.Response.Write(String.Format(" Scope is {0} and Windows.Identity is {1}. ", scope, WindowsIdentity.GetCurrent().Name));
            else
                context.Response.Write(String.Format(" Scope is {0} and Windows.Identity is null. ", scope));

            
            SearchExecutor s = new SearchExecutor();
            return s.ExecuteQuery(query);
        }

    }
The Thread.CurrentPrincipal passes the authentication test, but the value returned by WindowsIdentity.GetCurrent() changes to the search service account within the "DoSearch" task.

Free Windows Admin Tool Kit Click here and download it now
June 1st, 2015 2:07pm

Interesting. So why is the WindowsIdentity passed in? Only to impersonate the current identity if IsAuthenticated = false. However, in the case of your code, should not the WindowsIdentity.GetCurrent() return the application pool account running your handler from IIS and not the search service account? The task should be running in the W3wp.exe process.
June 1st, 2015 2:44pm

You might be right - in this case it's the same account as the search service account. And I basically copied that chunk of code from the ExecuteQueryTask method - I can't figure out either why pass in the WindowsIdentity if it doesn't get used - in fact, it seems like the code would work only if IsAuthenticated=false.
Free Windows Admin Tool Kit Click here and download it now
June 1st, 2015 4:26pm

I think I'm ready to close the book on this one since I've got a workaround, which is to not use SearchExecutor.ExecuteQueries but to mimic what it does in my own code. I still think that this is a bug and that the code in SharePoint's SearchExecutor.ExecuteQueryTask method needs to change to make use of the WindowsPrincipal passed into that method - without that, the security trimming does not seem to work. It would be great to hear from a Microsoft person on that issue. Thanks to Steve for your help figuring this out!

For the one person out there who might be using that ExecuteQueries method, here is my workaround:

After building the Dictionary<string,KeywordQuery> (mine is called allQueries) that would have been passed into ExecuteQueries, instead of calling that method I do this:

HttpContext context = HttpContext.Current;

Dictionary<string, ResultTableCollection> resultTableCollection = new Dictionary<string, ResultTableCollection>();

                if (allQueries.Count == 1) // If there is only one query in the dictionary, then just use ExecuteQuery
                {
                    SearchExecutor searchExecutor = new SearchExecutor();
                    string scope = allQueries.FirstOrDefault().Key;
                    KeywordQuery query = allQueries.FirstOrDefault().Value;
                    ResultTableCollection resultTables = searchExecutor.ExecuteQuery(query);
                    resultTableCollection.Add(scope, resultTables);
                }
                else // Otherwise, run them async
                {
                    Dictionary<string, Task<ResultTableCollection>> queryTasks = new Dictionary<string, Task<ResultTableCollection>>();
                    WindowsIdentity windowsIdentity = WindowsIdentity.GetCurrent(); // This needs to be passed into the task so that we don't lose it

                    foreach(KeyValuePair<string, KeywordQuery> kvp in allQueries)
                    {
                        Task<ResultTableCollection> task = Task<ResultTableCollection>.Factory.StartNew(
                            () => ExecuteSPSearchTask(context, windowsIdentity, kvp.Key, kvp.Value)
                            );
                        queryTasks.Add(kvp.Key, task);
                    }

                    Task.WaitAll(queryTasks.Values.ToArray<Task<ResultTableCollection>>());

                    foreach (KeyValuePair<string, Task<ResultTableCollection>> pair in queryTasks)
                    {
                        Task<ResultTableCollection> task = pair.Value;
                        if (task.Status != TaskStatus.Faulted)
                        {
                            resultTableCollection.Add(pair.Key, task.Result);
                        }
                        else
                        {
                            // If the task.Status is faulted, I just return an empty ResultTableCollection.
			    // Should probably log an error of some kind too.
                            resultTableCollection.Add(pair.Key, new ResultTableCollection());
                        }
                    }
                }

Then here is the ExecuteSPSearchTask method:

private ResultTableCollection ExecuteSPSearchTask(HttpContext context, WindowsIdentity windowsIdentity, string scope, KeywordQuery query)
        {
	    // This is the big piece missing from the SP code preventing results from being properly security trimmed            
	    WindowsImpersonationContext wic = windowsIdentity.Impersonate(); 
            
	    // I decided to add this in as a safety net, since I could pass in the HttpContext
	    if (Thread.CurrentPrincipal != context.User)
                Thread.CurrentPrincipal = context.User;           
            
            if (Thread.CurrentPrincipal.Identity.IsAuthenticated)
            {
                SearchExecutor searchExecutor = new SearchExecutor();
                return searchExecutor.ExecuteQuery(query);
            }
            else
            {
                // Do whatever you want here. I log a "Not Authenticated" message and then return an empty ResultTableCollection
                return new ResultTableCollection();
            }
        }

June 3rd, 2015 12:32pm

This topic is archived. No further replies will be accepted.

Other recent topics Other recent topics