Monday, September 17, 2007

Disable MS Office 2003 clipboard ring

If you don't know what I mean, go into any Office product since 2000 and type Control-c twice. You will get an annoying pane that steals all your valuable real estate and keeps coming back. I have not figured out how to disable it completely. The Office XP and Office 2000 fixes don't seem to affect the clipboard ring in Office 2003. What I have been able to do is make it so it does not bother me anymore. When it comes up, go to the options list and uncheck every option there. Basically, tell it to never show itself, never come up, never automatically do anything. It seems to be a global setting that affects all Office products with just one change. I hope this gives someone some sanity.

Wednesday, September 12, 2007

How to optimize HP OpenView Service Desk web-api calls

Open HP OpenvView Service Desk (OVSD). Go to the System administration module. In the tree select Data and then Web Api Application. Add a new item and give it a name. This is the name you will reference in your code. Add the attributes you will be using in your code. It is important to include as many of the columns as possible. Otherwise, they will be loaded on demand when you access them and this takes more time. Now that you have a Web Api Application defined in OVSD use the following code to use it in your code. public void SetWebApiApplication(ApiSDSession session, String appName) { IWebApiApplicationWhere where; IWebApiApplication[] applications; IWebApiApplication appl1; // Find the application mentioned in the argument. IWebApiApplicationHome applicationHome = session.getWebApiApplicationHome(); where= applicationHome.createWebApiApplicationWhere(); where.addCriteriumOnText(appName); applications = applicationHome.findWebApiApplication(where); if (applications == null) { System.out.println("There is no Web Api application called " + appName); return; } appl1= applications[0]; session.setApplicationSettings(appl1); } Essentially, when you use Web Api Application it is like doing the following in SQL. select col1, col5, col34 from MyLargeTable instead of select * from MyLargeTable You may not realize it, but I think OVSD also uses a "select" to do an update of data as well. The reason for this conclusion is that you must still search for the record you want to update, load the data into memory, make the modification, and then write change back to database. With that said, the biggest performance gain is going to be when you bring back many records instead of just one. There is still a performance gain for one or two records, but it is negligible in most cases because you have to specify the Web Api Application before you do the actual query.

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

Thursday, September 6, 2007

Impersonating a user programmatically in C# code

Here is how to call it.... public void SampleCallingMethod(Object s, EventArgs e) { if(ImpersonateUser("username", "domain", "password")) { //Insert your code that runs under the security context of a specific user here. doSomethingHere(); UnImpersonation(); } else { //Your impersonation failed. Therefore, include a fail-safe mechanism here. } } Here is the source code.... using System; using System.Web; using System.Web.Security; using System.Security.Principal; using System.Runtime.InteropServices; namespace AuthenticationMapper { /// /// Use this class to run a section of code as a different user. /// Most of this code was copied from: http://support.microsoft.com/kb/306158 /// Modifications by: Brent Vermilion 9/5/2007 /// public class Impersonation { WindowsImpersonationContext impersonationContext; #region WIN32 definitions public const int LOGON32_LOGON_INTERACTIVE = 2; public const int LOGON32_PROVIDER_DEFAULT = 0; [DllImport("advapi32.dll")] public static extern int LogonUserA(String lpszUserName, String lpszDomain, String lpszPassword, int dwLogonType, int dwLogonProvider, ref IntPtr phToken); [DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)] public static extern int DuplicateToken(IntPtr hToken, int impersonationLevel, ref IntPtr hNewToken); [DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)] public static extern bool RevertToSelf(); [DllImport("kernel32.dll", CharSet=CharSet.Auto)] public static extern bool CloseHandle(IntPtr handle); #endregion public Impersonation() { } #region SampleUsage public void SampleCallingMethod(Object s, EventArgs e) { if(ImpersonateUser("username", "domain", "password")) { //Insert your code that runs under the security context of a specific user here. UnImpersonation(); } else { //Your impersonation failed. Therefore, include a fail-safe mechanism here. } } #endregion public bool ImpersonateUser(String userName, String domain, String password) { WindowsIdentity tempWindowsIdentity; IntPtr token = IntPtr.Zero; IntPtr tokenDuplicate = IntPtr.Zero; if(RevertToSelf()) { if(LogonUserA(userName, domain, password, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, ref token) != 0) { if(DuplicateToken(token, 2, ref tokenDuplicate) != 0) { tempWindowsIdentity = new WindowsIdentity(tokenDuplicate); impersonationContext = tempWindowsIdentity.Impersonate(); if (impersonationContext != null) { CloseHandle(token); CloseHandle(tokenDuplicate); return true; } } } } if(token!= IntPtr.Zero) CloseHandle(token); if(tokenDuplicate!=IntPtr.Zero) CloseHandle(tokenDuplicate); return false; } public void UnImpersonation() { impersonationContext.Undo(); } } }