Monday 4 June 2007

Impersonation with Sharepoint Portal Server

Still another one for sharepoint security. In order to let our code to run, we have to know CAS(Code Access Security) , (see another one in my blog about CAS) to configure the permission of assemblies we made properly so sharepoint will know which assembly should access which part and how much of the system resources. Another level of this will be sharepoint in-build access control which is enforced by the sharepoint group and users. Take webpart for instance, if we put a webpart on the sites under the portal (assuming you are using SPS) and just access the content(list, document lib etc) but not in any subsites which doesn't use the same permission as this site, you will be fine without any impersonation. However, if you do want to use cross-site data to complete the logic in your webpart, say, you want to put your webpart in your portal front page and show the subsites data which you might not have permission. The purpose of this might be an overview or statistics for site information.You will have to come across the override of permission issues so you will get your code running without being stopped by security guard :) .

If you are using MOSS, you are lucky enough to be able to take advantage of in-build API method: RunWithElevatedPrivilege like this:


//using directives
using Microsoft.Sharepoint;
using Microsoft.Sharepoint.WebControls;

SPSecurity.CodeToRunElevated code = new SPSecurity.CodeToRunElevated(method_need_elevation);
SPSecurity.RunWithElevatedPrivilege(code);


public void method_need_elevation()
{...}


By using that, you can easily obtain the application pool identity.

In SPS, you have to hijack the system dlls in order to get hold of the windows identity which will be pointing to app pool. First of all, you need to make a class to contain this logic. Please note that we need to implement IDisposable interface so when the code exits the method, the impersonation will be restored to previous state. Also because we are using DLLImport, the system resource will not be garbage collected if we don't explicitly manually recycle the used objects. This will be mentioned later.

Namespaces needed in this article
we need to create the Windows Identity to represent the application pool account. WindowsIdentity object can be found within System.Security.Principal namespace, besides, as we need the entry point to the system dlls, System.Runtime.InteropServices namespace is needed for using DllImport attributed methods. At last we add in System.ComponentModel namespace to support win32Exceptions and application exceptions.

Code Explanation

As we discussed above, add in windows identity account variable _appPoolIdentity, and also the identitycontext objects for both current user's and app pool's . The reason of promoting these as member variables are later on we need to GC the unmanaged objects using these contexts as the reference.



using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Security.Principal;

public sealed class Identity: IDisposable
{

private static WindowsIdentity _appPoolIdentity;
private WindowsImpersonationContext _WindowsImpContext,_AppPoolImpSelfContext;





}

Then we need to import the dll and use the attributed methods DuplicateToken to copy the token of app pool to impersonate app pool's account.

//duplicate a token for code to impersonate
[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)]
private static extern bool DuplicateToken(IntPtr hToken_, int impersonationLevel_, ref IntPtr hNewToken_);


and of course, we need to manually release the resource used by token copying.


//free unmanaged allocated resources
[DllImport("kernel32.dll", CharSet=CharSet.Auto)]
private static extern bool CloseHandle(IntPtr handle);


now we are ready to make the application pool account property


private static WindowsIdentity AppPoolIdentity
{

get
{
lock(typeof(Identity))
{
if(_appPoolIdentity==null)
{
//get token for this identity
IntPtr token = WindowsIdentity.GetCurrent().Token;
if(token == IntPtr.Zero)
{
throw new ApplicationException("Cannot fetch the token of this application pool");
}

//duplicate token in order to impersonate
if(! DuplicateToken(token, 2, ref token))
{
throw new Win32Exception(Marshal.GetLastWin32Error(), "Unable to duplicate AppPool's identity token !");
}

//if didn't get the token, throw exception
if(token == IntPtr.Zero)
{
throw new ApplicationException("Unable to duplicate AppPool's identity token !");
}
// Store app pool's identity with the retrieved token
_appPoolIdentity = new WindowsIdentity(token);

//free the unmanaged resources
CloseHandle(token);
}
return _appPoolIdentity;
}
}
}


Then we make a call to execute impersonation using this apppool account identity. We can make a public constructor and write impersonation logic. Or We can make a static method to call a private constructor, which is much more elegant when using impersonation. Here it was using the second method.

//private constructor which will be called by the static impersonateAdmin method to impersonate
private Identity()
{
try
{
_AppPoolImpSelfContext= WindowsIdentity.Impersonate(IntPtr.Zero);
_WindowsImpContext = AppPoolIdentity.Impersonate();

}
catch
{

UndoImpersonate();
throw;
}
}


Note above that in case of exception occurs, the impersonate should be undo and therefore revert to previous user account identiy. This will be called in the manual GC process by simply recycle all the windowsidentitycontext we got from the return of impersonation. Notice not only the context needs to be set to null, but also the Undo() method of windowsidentycontext needs to be called to revert back to previous account.


private void UndoImpersonate()
{
//clear the context
if(_WindowsImpContext!=null)
{
_WindowsImpContext.Undo();
_WindowsImpContext = null;
}
if(_AppPoolImpSelfContext!=null)
{
_AppPoolImpSelfContext.Undo();
_AppPoolImpSelfContext = null;
}


}




We are almost there to finish the identity class, second to last thing is to implement IDisposable methods to carry out GC


public void Dispose()
{
CustDispose(true);
//self-controled Gabage collection
GC.SuppressFinalize(this);
}
public void CustDispose(bool _disposing)
{
if(_disposing)
{
this.UndoImpersonate();
}
}


and finally we write a public static method to execute the whole impersonation process, which is easy, we simply create a object of Identity class which calls the constructor.


public static Identity impersonateAdmin()
{
return new Identity();
}



Using Impersonation class we just made!

By using "Using" keyword, we surround the code that needs the permission of app pool account in the namespace we created using the class above and GC occurs after exits the namespace , the impersonation is done with breeze.


using (Identity impersonation = Identity.impersonateAdmin())
{
//code to run under impersonation context
}

No comments: