Thursday, 22 April 2010

Fixing the Precompilation Marker File Ektron Issue on IIS 7

After recently updating a 7.65 based website to 8.0 we recently found that parts of the workarea were unusable.  The upgrade went well and all the pre-deployment checks went well.  However, when we performed the UAT deployment the workarea was broken.  Where the treeview menu on the left should be, there was a helpful message:

This is a marker file generated by the precompilation tool, and should not be deleted!

Well, yes the UAT deployment is the first production quality deployment so precompilation is part of the build and this is part of the message that’s within all of the precompiled UI files (aspx, ascx, ashx, etc).  But on a precompiled site, the pages are actually stored in dll’s in the site’s bin folder – the actual files are a nicety to ensure IIS routes the request the ASP.Net.

Clearly this mechanism was working for aspx files as the frontend website worked as well as bits of the workearea.  After a bit of investigation, it appears that html and htm files are also being precompiled using the standard PageBuildProvider:

   1: <compilation debug="false">
   2:     <buildProviders>
   3:         <add extension=".htm"
   4:                  type="System.Web.Compilation.PageBuildProvider" />
   5:         <add extension=".html"
   6:                  type="System.Web.Compilation.PageBuildProvider" />
   7:     </buildProviders>
   8: </compilation>

The Fix

In the web.config ensure the following handler mappings  are added to configuration/system.webServer/handlers:

   1: <!-- 
   2: Add Under the aspx mapping for Integrated Mode 
   3: <add name="PageHandlerFactory-Integrated" path="*.aspx" verb="GET,HEAD,POST,DEBUG" type="System.Web.UI.PageHandlerFactory" preCondition="integratedMode"/>
   4:  -->
   5: <add name="Html-Integrated" path="*.html" verb="GET,HEAD,POST,DEBUG" type="System.Web.UI.PageHandlerFactory" preCondition="integratedMode"/>
   6: <add name="Htm-Integrated" path="*.htm" verb="GET,HEAD,POST,DEBUG" type="System.Web.UI.PageHandlerFactory" preCondition="integratedMode"/>
   7: <!-- Add under the aspx mapping for Classic Mode
   8: <add name="PageHandlerFactory-ISAPI-2.0" path="*.aspx" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness32" responseBufferLimit="0"/>
   9: -->
  10: <add name="Html-ISAPI-2.0" path="*.html" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness32" responseBufferLimit="0"/>
  11: <add name="Htm-ISAPI-2.0" path="*.htm" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness32" responseBufferLimit="0"/>
  12:  

The lines above are for IIS 7, if the same issue occurs on an earlier version then it should be trivial to add appropriate entries to configuration/system.web/httpHandlers.

It’s important that these handlers are registered prior to any wildcard handlers (path=”*.*”) and is mapped to the standard ASP.Net handler.  If it’s mapped to the Ektron EkDavHttpHandlerFactory then the html and htm files will be served as text and not processed as a serverside page.

Monday, 19 April 2010

Elevating Ektron User Permissions Safely

When you’re coding against the Ektron API you frequently find yourself needing to add/modify content as a result of a user action or similar privileged tasks.  To do this you need to impersonate a more privileged user (such as  InternalAdmin) for the duration of the task and then revert to the current users privileges.

The approach most frequently quoted on the Ektron dev forums is along the lines of:

   1: public void DoElevatedPermission()
   2: {
   3:    int currentCallerId;
   4:    int currentUserId;
   5:  
   6:    Ektron.Cms.CommonApi capi = new Ektron.Cms.CommonApi();
   7:    currentCallerId = cAPI.RequestInformationRef.CallerId;
   8:    currentUserId = cAPI.RequestInformationRef.UserId;
   9:  
  10:    // impersonate InternalAdmin
  11:    capi.RequestInformationRef.CallerId = Ektron.Cms.Common.EkConstants.InternalAdmin;
  12:    capi.RequestInformationRef.UserId = Ektron.Cms.Common.EkConstants.InternalAdmin;
  13:  
  14:    // Do work that requires elevated/impersonated permissions
  15:  
  16:    //set back to current user
  17:    capi.RequestInformationRef.CallerId = currentCallerId;
  18:    capi.RequestInformationRef.UserId = currentUserId;
  19: }

This works fine (as long as nothing goes wrong) but if an exception is thrown whilst performing the elevated method then there’s a risk that the rest of the request will run using the InternalAdmin permissions.  This could cause havoc!

So a smart approach would be:

   1: public void DoElevatedPermission()
   2:     {
   3:         int currentCallerId;
   4:         int currentUserId;
   5:         Ektron.Cms.CommonApi capi = new Ektron.Cms.CommonApi();
   6:         try
   7:         {
   8:             currentCallerId = cAPI.RequestInformationRef.CallerId;
   9:             currentUserId = cAPI.RequestInformationRef.UserId;
  10:  
  11:             //set impersonate InternalAdmin
  12:             capi.RequestInformationRef.CallerId = Ektron.Cms.Common.EkConstants.InternalAdmin;
  13:             capi.RequestInformationRef.UserId = Ektron.Cms.Common.EkConstants.InternalAdmin;
  14:  
  15:             // Do work that requires elevated/impersonated permissions
  16:         }
  17:         finally
  18:         {
  19:             //set back to current user
  20:             capi.RequestInformationRef.CallerId = currentCallerId;
  21:             capi.RequestInformationRef.UserId = currentUserId;
  22:         }
  23:     }

This guarantees that the real user is restored in any eventuality (where the request would be able to continue processing – clearly a finally block won’t protect your code from a meteor strike!).  But that’s a lot of boiler place code wrapping a single line representing work.  When you actually start doing work it’s going to get very complicated, very quickly!

Through a little IDisposable abuse it’s possible to replace a lot of this boiler plate code with a using statement, so the above code can be neatened up into something like this:

   1: public void DoElevatedPermission()
   2: {
   3:    Ektron.Cms.CommonApi contentApi = new Ektron.Cms.CommonApi();
   4:    using (ElevatedPermissionScope adminScope = new ElevatedPermissionScope(contentApi))
   5:    {
   6:        //Perform Elevated Tasks Here
   7:    }
   8:    // normal permissions have been restored
   9: }

Much neater.

Here’s the ElevatedPermissionScope implementation:

   1: using System;
   2: using Ektron.Cms;            // from Ektron.Cms.Common assembly
   3: using Ektron.Cms.Common;     // from Ektron.Cms.Common assembly
   4:  
   5: namespace MartinOnDotNet.Ektron.Security
   6: {
   7:  
   8:     /// <summary>
   9:     /// Utility class used to wrap a set of operations that must
  10:     /// be done within a more elevated security context than the current
  11:     /// user.
  12:     /// </summary>
  13:     /// <remarks>
  14:     /// <para>Due to implementation of the Ektron API each API implementation
  15:     /// must have it's own elevated scope wrapper.</para>
  16:     /// <para>Example of usage:</para>
  17:     /// <code>
  18:     /// ContentAPI contentApi = ApiFactory.Create&lt;ContentAPI&gt;();
  19:     /// using (ElevatedPermissionScope adminScope = new ElevatedPermissionScope(contentApi))
  20:     /// {
  21:     ///     //Perform Elevated Tasks Here
  22:     ///        
  23:     /// }
  24:     /// // perform normal tasks here
  25:     /// </code>
  26:     /// <para>
  27:     /// As this class manipulates <see ref="CommonApi" /> objects it can only
  28:     /// be used when there is a populated <see ref="System.Web.HttpContext" /> available.
  29:     /// </para>
  30:     /// </remarks>
  31:     public sealed class ElevatedPermissionScope : IDisposable
  32:     {
  33:         // Required Imports:
  34:         // using Ektron.Cms;            // from Ektron.Cms.Common assembly
  35:         // using Ektron.Cms.Common;     // from Ektron.Cms.Common assembly
  36:  
  37:  
  38:         /// <summary>
  39:         /// Initializes a new instance of the <see cref="ElevatedPermissionScope"/> class and configures
  40:         /// the latent userId to be the Ektron InternalAdmin user
  41:         /// </summary>
  42:         /// <param name="api">The API to elevate</param>
  43:         public ElevatedPermissionScope(CommonApi api)
  44:             : this(api, EkConstants.InternalAdmin, EkConstants.InternalAdmin)
  45:         { }
  46:  
  47:  
  48:         /// <summary>
  49:         /// Initializes a new instance of the <see cref="ElevatedPermissionScope"/> class and configures
  50:         /// the latent userId to the provided values
  51:         /// </summary>
  52:         /// <param name="api">The API to elevate</param>
  53:         /// <param name="callerId">The caller id to impersonate</param>
  54:         /// <param name="userId">The user id to impersonate</param>
  55:         public ElevatedPermissionScope(CommonApi api, long callerId, long userId)
  56:             : this(api.RequestInformationRef, callerId, userId)
  57:         { }
  58:  
  59:         /// <summary>
  60:         /// Initializes a new instance of the <see cref="ElevatedPermissionScope"/> class.
  61:         /// </summary>
  62:         /// <param name="requestInfo">The request info to configure</param>
  63:         /// <param name="callerId">The caller id.</param>
  64:         /// <param name="userId">The user id.</param>
  65:         public ElevatedPermissionScope(EkRequestInformation requestInfo, long callerId, long userId)
  66:         {
  67:  
  68:             if (requestInfo == null) throw new ArgumentNullException("requestInfo");
  69:             RequestInfo = requestInfo;
  70:             OriginalCallerId = RequestInfo.CallerId;
  71:             OriginalUserId = RequestInfo.UserId;
  72:             RequestInfo.CallerId = callerId;
  73:             RequestInfo.UserId = userId;
  74:             RequestInfo.UniqueId = 0;
  75:         }
  76:  
  77:         #region Internal Properties
  78:  
  79:         /// <summary>
  80:         /// Gets or sets the request info.
  81:         /// </summary>
  82:         /// <value>The request info.</value>
  83:         internal EkRequestInformation RequestInfo { get; set; }
  84:  
  85:         /// <summary>
  86:         /// Gets or sets the original caller id.
  87:         /// </summary>
  88:         /// <value>The original caller id.</value>
  89:         internal long OriginalCallerId { get; set; }
  90:  
  91:         /// <summary>
  92:         /// Gets or sets the original user id.
  93:         /// </summary>
  94:         /// <value>The original user id.</value>
  95:         internal long OriginalUserId { get; set; }
  96:  
  97:         #endregion
  98:  
  99:         #region IDisposable Members
 100:  
 101:         /// <summary>
 102:         /// Restores the original Latent User Id
 103:         /// </summary>
 104:         public void Dispose()
 105:         {
 106:             RequestInfo.CallerId = OriginalCallerId;
 107:             RequestInfo.UserId = OriginalUserId;
 108:             GC.SuppressFinalize(this);
 109:         }
 110:  
 111:         #endregion
 112:     }
 113:  
 114: }