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: }

3 comments:

  1. Hey Martin,

    Excellent post - not just on implementing Ektron impersonation safely, but the 'using' statement and disposable objects as well.

    I do wonder why you would recommend impersonation when current APIs run as InternalAdmin already. If you want to use elevated API privileges, I would recommend using Ektron.Cms.API.Content.Content rather than Ektron.Cms.ContentAPI. Would you not agree?

    ReplyDelete
  2. I've found that the actual implementation detail can vary between the similar objects - usually in the amount of detail populated within objects. It's usually a case of trying the alternatives to ensure you get what you need.

    The class provided will work with most of the API classes as there's an overload to accept an EkRequestInformation class - as well as specifc user ids.

    ReplyDelete
  3. This method isn't so great for impersonating other workarea users. For that I've created the ImpersonationScope!

    http://bit.ly/cBJETc

    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!