Wednesday, 21 July 2010

Ektron: Safely Impersonating Users within Ektron

I recently hit an scenario where I needed to impersonate a lesser privileged workarea user to perform some content creation/approvals whilst working within the Approval Chain.

My initial attempts leveraged my ElevatedPermissionScope class, which failed with an error similar to:

   1: System.Exception: Index was outside the bounds of the 
   2: array.[EkSite-->;GetPermissions(Int,Int,Int,String,[&Collection)]][EkSite->GetPermissions(Int,Int,String,[&Collection]]
   3:  at 
   4: Ektron.Cms.EkException.ThrowException(Exception ex, EventLogEntryType EventType)
   5:  at 
   6: Ektron.Cms.EkException.ThrowException(Exception ex)
   7:  at 
   8: Ektron.Cms.ContentAPI.CheckOutContentById(Int64 ContentId)
   9:  at 
  10: Freestyle.Helpers.Ektron.ContentTesting.EktronApiQuery.CheckOutContent(Int64 
  11: contentId, Int32 cultureLcid, Int64 userId)

Now the error is suitable vague that it made diagnosing the issue next to impossible.  After a lot of experimentation with different API calls and approaches (thank you eGandalf), I was ready to throw my toys out of the pram and go home when eventually Ektron support provided the following snippet that seemed to do what I was looking for:

   1: Dim userAPI As New Ektron.Cms.API.User.User
   2: Dim userObj As Ektron.Cms.UserData
   3: userObj = userAPI.LogInUser("jedit", "jedit", "", "", "")
   4: userAPI.SetAuthenticationCookie(userObj)
   5: Dim cd As New Ektron.Cms.ContentAPI
   6: cd.DeclineContent(104)
   7: Dim cook As HttpCookie = Request.Cookies("ecm")
   8: cook.Expires = Now()
   9: Response.Cookies.Add(cook)

A quick bit of experimentation proved that this would solve my problem, with a couple of side affects:

  1. Automatically logging in as a person has some side affects….if the password changes the account could be locked very quickly!
  2. At the end of the operation the user would be logged out of the workarea

Well, there’s not much I could do about point 1.  But I could work a bit of IDisposable Scope magic to manage the changes to the ‘ecm’ cookie which stores the login details for the current session.

Introducing the ImpersonationScope

This scope works in a very similar manner to the ElevatedPermissionScope in that you wrap the block of code you want to execute as the impersonated user in a using statement containing the ImpersonationScope object you want to use.

   1: public bool ApproveContent(long contentId, int cultureLcid, string userName, string password, string domain)
   2: {
   3:    if (string.IsNullOrEmpty(userName)) throw new ArgumentNullException("userName");
   4:    if (string.IsNullOrEmpty(password)) throw new ArgumentNullException("password");
   5:    if (domain == null) domain = string.Empty;
   6:    CultureInfo locale = new CultureInfo(cultureLcid);
   7:    using (new ImpersonationScope(userName, password, domain))
   8:    {
   9:        global::Ektron.Cms.ContentAPI api = new global::Ektron.Cms.ContentAPI()
  10:        api.ContentLanguage = locale.LCID;
  11:        return api.ApproveContent(contentId);
  12:    }
  13: }

When the executing code leaves scope, the impersonated credentials are automatically replaced with the previous set.

So, without further waffle - here’s the implementation:

   1: using System;
   2: using System.Web;
   3: using Ektron.Cms;
   4:  
   5: namespace MartinOnDotNet.Helpers.Ektron.Security
   6: {
   7:     /// <summary>
   8:     /// Utility class to temporarily login as another user
   9:     /// </summary>
  10:     public sealed class ImpersonationScope : IDisposable
  11:     {
  12:  
  13:         /// <summary>
  14:         /// Initializes a new instance of the <see cref="ImpersonationScope"/> class.
  15:         /// </summary>
  16:         /// <param name="userName">Name of the user.</param>
  17:         /// <param name="password">The password.</param>
  18:         /// <param name="domain">The domain.</param>
  19:         public ImpersonationScope(string userName, string password, string domain)
  20:         {
  21:             if (string.IsNullOrEmpty(userName)) throw new ArgumentNullException("userName");
  22:             if (string.IsNullOrEmpty(password)) throw new ArgumentNullException("password");
  23:             if (domain == null) domain = string.Empty;
  24:             SetEcmCookieUserId(userName, password, domain);
  25:         }
  26:  
  27:         /// <summary>
  28:         /// Gets or sets the user id.
  29:         /// </summary>
  30:         /// <value>The user id.</value>
  31:         public long UserId { get; set; }
  32:  
  33:  
  34:         private static HttpCookie _ecm;
  35:  
  36:         private void SetEcmCookieUserId(string userName, string password, string domain)
  37:         {
  38:             if (HttpContext.Current != null
  39:                 && HttpContext.Current.Request != null)
  40:             {
  41:                 _ecm = HttpContext.Current.Request.Cookies["ecm"];
  42:                 HttpContext.Current.Request.Cookies.Remove("ecm");
  43:                 global::Ektron.Cms.API.User.User u = new global::Ektron.Cms.API.User.User();
  44:                 UserData ud = u.LogInUser(userName, password, string.Empty, domain);
  45:                 u.SetAuthenticationCookie(ud);
  46:                 UserId = ud.Id;
  47:             }
  48:             else
  49:             {
  50:                 throw new NotSupportedException("Must be called within a valid Web Context!");
  51:             }
  52:         }
  53:  
  54:         private static void RevertEcmCookie()
  55:         {
  56:             if (HttpContext.Current != null
  57:                 && HttpContext.Current.Request != null)
  58:             {
  59:                 HttpContext.Current.Request.Cookies.Remove("ecm");
  60:                 HttpContext.Current.Request.Cookies.Add(_ecm);
  61:                 HttpContext.Current.Response.Cookies.Remove("ecm");
  62:                 HttpContext.Current.Response.Cookies.Add(_ecm);
  63:             }
  64:         }
  65:  
  66:         /// <summary>
  67:         /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
  68:         /// </summary>
  69:         public void Dispose()
  70:         {
  71:  
  72:             RevertEcmCookie();
  73:             GC.SuppressFinalize(this);
  74:         }
  75:  
  76:     }
  77: }

Enjoy.

Tuesday, 20 July 2010

Changing A Master Page Body Tag’s CSS Class For Different Content Pages

It seems a bit of a failing of Master Pages that there’s no clear way to assign different CSS classes to the ‘body’ tag based on the Content Page.  To get around this, I’ve taken to inheriting all of my content pages from a known base class (which in turn inherits from System.Web.Page) and then giving it a publically accessible property of ‘BodyCssClass’!

   1: namespace MartinOnDotNet.MasterPageBodyClass
   2: {
   3:     public class BasePage : System.Web.UI.Page
   4:     {
   5:         public string BodyCssClass { get; set; }
   6:  
   7:     }
   8: }

This can then be picked up from the MasterPage and used to modify the body tag:

   1: using System;
   2:  
   3: namespace MartinOnDotNet.MasterPageBodyClass
   4: {
   5:     public partial class Global : System.Web.UI.MasterPage
   6:     {
   7:         protected override void OnPreRender(EventArgs e)
   8:         {
   9:             base.OnPreRender(e);
  10:             BasePage current = Page as BasePage;
  11:             if (current != null && !string.IsNullOrEmpty(current.BodyCssClass))
  12:             {
  13:                 Body.Attributes["class"] = current.BodyCssClass;
  14:             }
  15:         }
  16:     }
  17: }
   1: <%@ Master Language="C#" AutoEventWireup="true" CodeBehind="Global.master.cs" Inherits="MartinOnDotNet.MasterPageBodyClass.Global" %>
   2: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
   3: <html xmlns="http://www.w3.org/1999/xhtml" >
   4: <head runat="server">
   5:     <asp:ContentPlaceHolder ID="head" runat="server">
   6:     </asp:ContentPlaceHolder>
   7: </head>
   8: <body runat="server" id="Body">
   9:     <form id="form1" runat="server">
  10:     <div>
  11:         <asp:ScriptManager ID="ScriptManager1" runat="server" />
  12:         <asp:ContentPlaceHolder ID="ContentPlaceHolder1" runat="server">
  13:         
  14:         </asp:ContentPlaceHolder>
  15:     </div>
  16:     </form>
  17: </body>
  18: </html>

The class itself can be assigned in the <%@ Page %> declaration of the Content Pages that implement the MasterPage:

   1: <%@ Page Language="C#" 
   2:     MasterPageFile="~/Global.Master" 
   3:     AutoEventWireup="true" 
   4:     Title="Content Page" 
   5:     Inherits="MartinOnDotNet.MasterPageBodyClass.BasePage"
   6:     BodyCssClass="content" %>
   7:  
   8: <asp:Content ID="Content1" ContentPlaceHolderID="head" runat="server">
   9: </asp:Content>
  10: <asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" runat="server">
  11: </asp:Content>

Attachment: MasterPageBodyClass.zip