Friday, September 7, 2007

Moving SharePoint to a new active directory domain

Let's pretend that your company has been bought or you bought another company that has SharePoint installed and already being used. The active directory team decides that they want to duplicate the current usernames and copy them to a new active directory domain. No problem they think. As you will soon find out, SharePoint doesn't like that idea without some migration of users. If you are fortunate enough to be able to convert all the users at once and never use the old domain in the meantime then you are in luck. Microsoft in Service Pack 2 for SharePoint has included functionality to make you job easier. stsadm command line utility should do it for you. Just use the migrateuser option. Call that for each user and you should be in good shape. If however, you do not know what users will be migrated to the new domain, and when, and if the old domain will still be used, need a back out plan, etc then this strategy may not work unless you want to be fielding calls from users and migrating them as they have users. This is hardly a proactive approach. HttpModules come to the rescue. SharePoint is an asp.net application, so it has web.config. Web.config files allow us to add custom HttpModules to SharePoint. HttpModules essentially allow us to handle events before the application does. So, what I propose is to look at the username and domain coming in. If the domain of the current user is the same as what is in SharePoint database (see the UserInfo table in the SharePoint SITES database) then we do nothing. If however they are different then we migrate the SharePoint User information to be the current user's domain. This does a couple of things. First, it makes it so both old and new domains can access SharePoint. Second, it allows users to be migrated when ever they hit the site so we don't have to know when they will be migrated, rollback if the active directory team decides to roll back their plan, etc. It is a flexible plan for all, and best of all it is a proactive approach. As it turns out SharePoint has a SharePoint.dll that allows us to call this migration tool from c#. So, all we need to do is create a MS Visual Studio 2003 class library, reference the SharePoint.dll in the project, sign out dll, install our new HttpModule into SharePoint and we are done. To install the HttpModule into SharePoint, there are a couple of places we need to make changes. The appSettings will need to be customized to work in your environment, HttpModules section will need to be changed to use your PublicKeyToken and version number. Make the following changes C:\Program Files\Common Files\Microsoft Shared\web server extensions\60\TEMPLATE\LAYOUTS\web.config
  <configSections>
    <section name="appSettings" type="System.Configuration.NameValueFileSectionHandler, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
  </configSections>
  <appSettings>
    <!-- AuthenticationMapper Configuration Start -->

    <add key="SharePointSiteDatabaseConnectionString" value="Data Source=mydbserver;Initial Catalog=SP_SITE;User ID=AuthenticationMapper;Password=AuthenticationMapperPassword;" />
    <add key="PrivilegedUserDomain" value="mydomain" />
    <add key="PrivilegedUserName" value="me" />
    <add key="PrivilegedUserPassword" value="yourpassword" />

    <!-- AuthenticationMapper Configuration End -->
  </appSettings>
  <httpModules>
    <clear />
    <add name="OutputCache" type="System.Web.Caching.OutputCacheModule" />
    <add name="WindowsAuthentication" type="System.Web.Security.WindowsAuthenticationModule" />
    <add name="AuthenticationMapper" type="AuthenticationMapper.AuthenticationMapper,AuthenticationMapper, Version=1.0.0.0, Culture=neutral, PublicKeyToken=c03821ca1e881e5a, Custom=null"/>
  </httpModules>


Depending on where you set the web root for SharePoint Portal Server 2003, you will want to edit the web.config there. Check IIS if you are not sure. It is the location that your IIS website points to. C:\Inetpub\SharePoint\web.config
  <appSettings>
    <!-- AuthenticationMapper Configuration Start -->

    <add key="SharePointSiteDatabaseConnectionString" value="Data Source=mydbserver;Initial Catalog=SP_SITE;User ID=AuthenticationMapper;Password=AuthenticationMapperPassword;" />
    <add key="PrivilegedUserDomain" value="mydomain" />
    <add key="PrivilegedUserName" value="me" />
    <add key="PrivilegedUserPassword" value="yourpassword" />

    <!-- AuthenticationMapper Configuration End -->
  </appSettings>
  <httpModules>
    <clear />
    <add name="OutputCache" type="System.Web.Caching.OutputCacheModule" />
    <add name="WindowsAuthentication" type="System.Web.Security.WindowsAuthenticationModule" />
    <add name="AuthenticationMapper" type="AuthenticationMapper.AuthenticationMapper,AuthenticationMapper, Version=1.0.0.0, Culture=neutral, PublicKeyToken=c03821ca1e881e5a, Custom=null"/>
  </httpModules>
Our dll must be Signed in order for SharePoint to accept it. Signing a dll is beyond the scope of this blog, but there are lots of good articles. Just google it. ;) I do recommend you hard code the version number (at least during devlelopment). If you don't you will need to change the two web.configs above everytime you redeploy. If you have a development server with Visual Studio 2003 on it you can develop directly on the server. This includes debugging. I recommend it if you can do it. If not, you can develop on your local machine, copy the dll to the server after it builds. In either case, you will want to uninstall the previous dll from the gac, then install the new one, then reset IIS (unless you change the version everytime) because IIS doesn't see that the dll in the gac changed since the version didn't change. Here is the command line way to uninstall, and install our dll into the gac. C:\WINDOWS\Microsoft.NET\Framework\v1.1.4322\gacutil /u AuthenticationMapper C:\WINDOWS\Microsoft.NET\Framework\v1.1.4322\gacutil -I "c:\AuthenticationMapper.dll" C:\WINDOWS\Microsoft.NET\Framework\v1.1.4322\gacutil -l AuthenticationMapper iisreset I recommend calling iisreset as well. I don't think it is necessary if you change the version everytime, but I didn't so I needed iisreset so that IIS would see the new dll. Here is the class you will need in your project. This code works, but error handling is left to you to decide how to handle things. There are references to Config.Instance.xxx. This is a class that just accesses the web.config. So, yes, you can access the web.config of the application we are plugging into (in this case SharePoint).
start code

using System;
using System.Web;
using System.IO;
using System.Security;
using System.Data.SqlClient;
using System.Data;
using Microsoft.SharePoint.Administration;
using System.Collections;
using System.Web.Mail;
using System.Diagnostics;
namespace AuthenticationMapper
{
   /// <summary>
   /// Summary description for Class1.
   /// </summary>
   public class AuthenticationMapper : IHttpModule
   {
      // IHttpModule members
      public void Init(HttpApplication httpApp)
      {
         httpApp.AuthenticateRequest += new EventHandler(this.OnAuthenticateRequest);
      }
      public void Dispose()
      {
         // Usually, nothing has to happen here...
      }
      // event handlers 
      public void OnAuthenticateRequest(object o, EventArgs ea)
      {
 
         string url = Url;
         bool neededMigration = false;
         try
         {
            if (User == null) throw new Exception("User was null");
          
            if (!url.ToUpper().EndsWith("SPSCRAWL.ASMX") && !url.ToUpper().EndsWith("SITEDATA.ASMX"))
            {
               string currentLogon = User.Identity.Name;
               // if it isn't the Network Service making request then try to migrate it
               // I think the Network Service is for the SharePoint indexer
               if (currentLogon.ToUpper() != @"NT AUTHORITY\NETWORK SERVICE")
               {
                  string currentUsername = this.CurrentUserNameOnly(User.Identity.Name);
                
                  string[] sharepointLogons = GetSharePointLogon(currentUsername);
                  for (int i=0; i<sharepointLogons.Length; i++)
                  {
                     try
                     {
                        // if Current User credentials are different from the ones in SharePoint
                        // then migrate user in SharePoint to match the Current User credentials (i.e. change domain)
                        if (sharepointLogons[i].ToUpper() !=  currentLogon.ToUpper())
                        {
                           MigrateUser(currentLogon, sharepointLogons[i]);
                           neededMigration = true;
                        }
                     }
                     catch (Exception ex)
                     {
                        // handle error here
                     }
                  }
               }
            }
         }
         catch (Exception ex)
         {
            // handle error here
         }
         // sometimes the security context or something gets messed up (on certain pages) after the migration.
         // Redirect seems to fix it.
         if (neededMigration)
         {
            HttpContext.Current.Response.Redirect(url);
         }
      }
      
      // update references to sharepointLogon in SharePoint to be currentLogon
      public void MigrateUser(string currentLogon, string sharepointLogon)
      {
         Impersonation su = new Impersonation();
         if(su.ImpersonateUser(Config.Instance.PrivilegedUserName, Config.Instance.PrivilegedUserDomain, Config.Instance.PrivilegedUserPassword)) 
         {
            // Migrate user to new domain.
            // For more info: http://support.microsoft.com/kb/896593
            SPGlobalAdmin wss = new SPGlobalAdmin();
       
            wss.AllowUnsafeUpdates = true; // we need this so the update will be allowed
            bool enforceSidHistory = false;
            wss.MigrateUserAccount(sharepointLogon, currentLogon, enforceSidHistory);
            //Insert your code that runs under the security context of a specific user here.
            su.UnImpersonation();
         }
         else
         {
            throw new Exception("Could not impersonate a user that can run stsadm.exe.");
         }
      }
    
      // username should not include domain
      public string[] GetSharePointLogon(string username)
      {
         ArrayList logons = new ArrayList();
         SqlDataReader reader;
       
         string connStr = Config.Instance.SharePointSiteDatabaseConnectionString;
         using (SqlConnection conn = new SqlConnection(connStr))
         {
            conn.Open();
            // SharePoint doesn't actually every delete a user, it just marks it as deleted. If the user is added again, then it is "undeleted" instead of added.
            string sql = string.Format(@"select distinct tp_login from dbo.UserInfo where tp_deleted = 0 and tp_Login like '%\{0}'", username);
            SqlCommand cmd = new SqlCommand(sql, conn);
            cmd.CommandType = CommandType.Text;
            reader = cmd.ExecuteReader();
            using (reader)
            {
               while (reader.Read())
               {
                  logons.Add(Convert.ToString(reader[0]));
               }
            }
         }
         return (string[])logons.ToArray(typeof(string));
      }
      public string CurrentUserNameOnly(string logon)
      {
         return logon.Split(new char[]{'\\'})[1];
      }
      public string CurrentDomainOnly(string logon)
      {
         return logon.Split(new char[]{'\\'})[0];
      }
    
 
 
      public System.Security.Principal.IPrincipal User
      {
         get
         {
            return HttpContext.Current.User;
         }
      }
     
      public string Url
      {
         get
         {
            string val = HttpContext.Current.Request.Url.ToString();
            return val;
         }
      }   
}
}
end code

15 comments:

Anonymous said...

This looks really useful. We have a combination of indivduals and domain global groups assigned to our sharepoint groups. How do we handle the migration of the domain global groups?

Brent V said...

That is a great question. I think the best approach is to a both group from both domains. That way they will both have access. Microsoft doesn't provide a solution as they do with individual users, so I don't know of any automated way to do this, though I have not researched it. Once the both groups are in the access list SharePoint should allow them to access the site then. Once SharePoint authenticates them, the code described here should kick in and convert the information related to that user. I have not tried it, but that is my hypothesis right now. :) Let me know how it goes if you try it.

Anonymous said...

I wanted to move the server to a new domain in a different forest (2 way trusts enabled). If i simply joined the server to the new domain, will it still work since 2 way trusts are enabled?

Brent V said...

Sorry for the delay, I was on my honeymoon. There are generally two issues when moving SharePoint.

1. The users also change domain. If this is not the case for you then this solution really doesn't apply to you.

2. The url to access SharePoint changes. In that case you will need to search the database and replace all references to the old url with the new url.

To specifically address your situation, my guess would be that if the url and the users are still the same and the domains have full 2 way trust then you should not have any issues. Let me know how it goes.

Anonymous said...

Does the -migrateusers STSADM switch also handle changing domains for service accounts?

Brent V said...

Hi anonymous,

If by service account you are still talking about an Active Directory account and it exists in both the old and the new domain, then migration should work fine.

I hope that answers your question.

Regards,

Brent

Peace-o-vutionalist said...

I am creating a test server to test our disaster recovery process. Therefore, we tried to replicate the environment (to an extent) by having 1 frontend and 1 sql machine (same structure as Live environment, but test machines are 2 virtual machines) the Test environment Front end is on - Windows server 2008/WSS 3.0 SP2 and backend SQL server 2008 while live is on Windows server 2003/WSS 3.0 SP1 and SQL server 2005 respectively; also we kept the domain name on test server exactly same as on Live.
We thought since domains are same so there won’t be any issues if we manually create the users in Test AD with the same permissions as on Live AD. But, when restored the application (using stsadm) it appeared that the Test server is not recognizing those accounts manually created in the test server AD.
Some one said that there is a hidden unique id associated to each user and when you manually creates a user it got change may be that's why it’s causing issues, therefore he suggested migrating the users through ADMT v3.1 tool by Microsoft, but I am still trying to understand nothing done yet except installed it on test environment. Also, I DON’T want to install any thing on live server as far as it’s totally inevitable. Can you suggest any solution how should I go about step by step doing the migration of all the objects in source AD to Target AD without causing any changes to Source AD or environment. Please reply me soon I am kinda stuck.

Adnan Shamim said...

why do we have to make changes in the web config file located at C:\Program Files\Common Files\Microsoft Shared\web server extensions\60\TEMPLATE\LAYOUTS\web.config

btw, i think the correct location is

C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\LAYOUTS\web.config

?

can we just make http handler changes in the the sharepoint web application we intend to place this functionality in ?

i have quite a few sites for various entities in my environment and i do not want to make any physical file changes in the 12 hive.

Adnan Shamim said...

can you please confirm if the code targets moss 2007 or SPS 2003 ?

Brent V said...

Adnan,

This entry is for SPS 2003. When I wrote this there was no MOSS 2007. You can tell because the paths show 60, not 12. 12 is for MOSS 60 is for SPS 2003.

I hope that helps.

Brent

Brent V said...

Syed,

I have never done what you are describing. I suspect if you moved to a different domain name it would actually be easier because then you could use the command line stsadm -o migrateuser to migrate the user. The command also has an ignoresid or something like that. I think this will ignore the id you are talking about. I don't know if the migrate tool works when the domain is the same though.

I hope that helps or you have found a solution.

Brent

Frank Kahle said...

we have a farm of sharepoint 2007 servers in our domain uw.private, we want to create a subdomain called portal.uw.private. there are no active users in the farm yet. what do we have to do to make this happen? All of the sharepoint servers use a backend sql 2008 server for the database and it will move to the new domain as well.

Brent V said...

Hi Frank,

It depends on how you want to approach the problem. On one hand you can follow what I have written here if you want to do a gradual migration and the usernames will stay the and only the domain will change.

On the other hand, if you have the luxury of just migrating the users all at once it is much easier. In this case, you should look stsadm.exe and the migrateuser option.

The big problem you will have is if the url changes. In most cases it will and that means you will need to do search and replace in the database itself, or use the api (.Net) to convert all the links that are absolute urls. Any urls that are relative will be fine though. If you can just make your site owners update the links then that saves the database search and replace / api stuff. Please be warned search and replace in the database will void your Microsoft support agreements in most cases.

If your question is more of how do you move SharePoint itself I would recommend a full backup of your content database. Install SharePoint from scratch when it is on the new domain. Restore the content database, and point to it when you do the install of SharePoint. It is a royal pain in the butt, but in the end I seem to have less issues if I just do a clean install. Keep in mind, you will lose all your indexes for searching.

Your question really deserves many pages of explanation, but this is the overview. :) Good luck.

Unknown said...

Thank you for the thorough explanation, about how to move a SharePoint to a new active directory domain. I can really make great use of the code you gave, I just have to twig it here and there to match.

moving company

home removals said...

You have a very interesting way of expressing yourself, which I like and as much as I can see so do other, visitors. Keep up the good work with such relevant information.