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