SPUserCollection.Remove() throws misleading exception “Cannot complete this action”

 

Over the weekend we performed a User Migration of our primary SharePoint 2010 farm.  In our case, which is probably a quite common scenario, we had users from both the source and target domain that we were migrating actively using the system.  In our scenario, the “TARGET” domain is the company that acquired us and our portals in the “SOURCE” domain.  New users from the target domain needed to access our portals in the source domain, and our source domain users needed to continue business as usual until we were prepared to migrate source users into the target domain.

Of course, with any company acquisition, there are many levels of migration steps, and one of those for us was the users actual AD accounts were migrated months ago, so they use TARGET\user as their primary logon, but when accessing the SharePoint sites, they re-log in with their current SOURCE\user until those accounts in SharePoint are migrated, but this left a problem we had to work around, and that problem being that the users had hit SharePoint with their TARGET domain account, and thus had both a reference to their SOURCE domain account and their TARGET domain account in SharePoint.  You can’t call the SharePoint Migrate commands to migrate a user from the source to the target, if the target already exists.  It will fail since the target user already exists.

Therefore, what we needed to do delete the target domain instances of their account in SharePoint before migrating their source account. 

Doing that from the object model is a very simple process. You just enumerate over every site collection, and call SPUserCollection.Remove() using the SiteUsers collection of the RootWeb in the site collection.  The only problem that we ran in to, was something I’ve actually seen a long long time ago, but never thought to actually document the problem before.

 

Consider the following section of code:

  
foreach (SPWebApplication wa in SPWebService.ContentService.WebApplications)
{
  WritePurgeOutput("Examining web application: " + wa.Name);
  // Enumerate all site collections in the web application
  foreach (SPSite sc in wa.Sites)
  {
  try
  {
    // Get the root web, so that we can get to SiteUsers.
    // SiteUsers represents all users in the site collection.
    using (SPWeb web = sc.RootWeb)
    {
      foreach (SharePointUser user in users)
      {
        WritePurgeOutput("Removing login: " + user.LoginName);
        try
        {
          web.SiteUsers.Remove(user.LoginName);
        }
        catch (Exception ex)
        {
          WritePurgeOutput(string.Format("ERROR - purging User '{0}' " +
           "from site collection '{1}': {2} ", 
           user.LoginName, sc.Url, ex.ToString()));
        }
      }
    }
  }
  catch (Exception ex)
  {
    WritePurgeOutput(string.Format("ERROR - purging Users " +
      "from site collection '{0}': {1} ", 
      sc.Url, ex.ToString()));
  }
  finally
  {
    sc.Dispose();
  }
}

 

The foreach(SharePointUser user in users) section is a collection of user objects containing the information for the users we want to purge from the system.  We had a prepared list of all the users that were AD migrated, and the user collection information was their  login name in the TARGET\user formation. WritePurgeOutput() is just a method that’s going to …we… write purge output.

In testing, this worked flawlessly.   I had some of my test members in the site, and deleted them, then ran this code while some were still permissioned.   For those that were still permissioned, they were removed, and for those that didn’t exist anymore, it still just called SiteUsers.Remove() without any problems.  In production though, we ran into an interesting exception.

We started noticing the following exception being thrown when the Remove() call was made:

"Cannot complete this action.\n\nPlease try again."

Our first thought was there there was something wrong in the access rights granted to the account we were using while logged onto the server to run our tool. (See http://blog.krichie.com/2008/09/11/unrestricted-access-via-sharepoint-object-model-from-console-applications/) But we did see successful executions as well, so that didn’t appear to be the problem.

It turns out, that Remove() only throws an exception if the user truly had NEVER visited the site collection (or was added) and therefore, there was NO record in the Users table.

YET, when a user DOES exist in the Users table, but was previous deleted (the tp_Deleted column has a positive value), the Remove() method just silently returns.  In other words, the user TRULY does not exist as an active user for the site collection, but that users information still exists in the database associated to the site collection.  He’s just marked deleted.  This made us think that SPUserCollection.Remove() would just silently return when called for a login that didn’t exist in the site.

Instead, our logs were filled with the exception message noted above, and leading us down a path of misguided troubleshooting.

 

In the end, we modified the code to look something similar to the following:

 

  
foreach (SPWebApplication wa in SPWebService.ContentService.WebApplications)
{
  WritePurgeOutput("Examining web application: " + wa.Name);
  // Enumerate all site collections in the web application
  foreach (SPSite sc in wa.Sites)
  {
  try
  {
    // Get the root web, so that we can get to SiteUsers.
    // SiteUsers represents all users in the site collection.
    using (SPWeb web = sc.RootWeb)
    {
      foreach (SharePointUser user in users)
      {
        WritePurgeOutput("Removing login: " + user.LoginName);
        try
        {
          SPUser userexists = null;
          userexists = web.SiteUsers[user.LoginName);
          if(userexists != null)
            web.SiteUsers.Remove(user.LoginName);
        }
        catch (Exception ex)
        {
          if(ex.Message != "User cannot be found.")
            WritePurgeOutput(string.Format("ERROR - purging User '{0}' " +
             "from site collection '{1}': {2} ", 
             user.LoginName, sc.Url, ex.ToString()));
        }
      }
    }
  }
  catch (Exception ex)
  {
    WritePurgeOutput(string.Format("ERROR - purging Users " +
      "from site collection '{0}': {1} ", 
      sc.Url, ex.ToString()));
  }
  finally
  {
    sc.Dispose();
  }
}

 

So we look for the user in the collection, and if it throws an exception with a message of “User cannot be found”, we just eat that and move on.   If we did find the user, then we make the call to Remove().

A nice exception message of “User cannot be found” on the call to Remove() in the first place, would have been really helpful.

 

– Keith

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s