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<ContentAPI>();
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: }
Hey Martin,
ReplyDeleteExcellent 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?
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.
ReplyDeleteThe 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.
This method isn't so great for impersonating other workarea users. For that I've created the ImpersonationScope!
ReplyDeletehttp://bit.ly/cBJETc