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.

2 comments:

  1. Hi Martin, I tried using your class and it does what it's supposed to. But it leaves the admin user logged in even after replacing the cookie. Is there a workaround for this? Regards, Mike.

    ReplyDelete
  2. Hmm, it shouldn't do this provided the Dispose() method is called. It certainly doesn't on 7.66> onwards as I've got several project that use it without leaving the admin user logged in.

    Are you certain that the Dispose() method is called? What version of Ektron are you using? Can you provide a code sample which has the issue?

    ReplyDelete

Got something to say? Let it out then!
Comments are moderated, so it may take a while to for them to be displayed here!