Asked by:
Plugin not running as impersonated user but as the calling user instead

Question
-
I'm creating a plugin for MS CRM 2015 using the CRM 2013 Developer Toolkit for VS 2012. I've created a Dynamics CRM 2013 Solution, removed the workflow project and then added a plugin to the Plugins project. The aim of the plugin is to get a number from the 'autonumber' entity, add 1, update the opportunity with this number and then update the 'autonumber' entity with the new number. However, the calling user won't have the right privileges to update the 'autonumber' entity so the plugin needs to run in the context of a sys admin user that's been created for this job.
I selected the user when creating the plugin and they can be seen in the Plugin Registration Tool as well, but it never runs as them. Here is my code:
public class GenerateOpportunityAutoNumber: Plugin //IPlugin { #region Constant Values private const string OPPORTUNITY_REF_ID = "new_salesnumber"; private const string OPPORTUNITY_ENTITY_NAME = "opportunity"; private const string OPPORTUNITY_PRIMARY_KEY = "opportunityid"; private const string LAST_REF_ENTITY_NAME = "new_opportunityautonumber"; private const string LAST_REF_PRIMARY_KEY = "new_opportunityautonumberid"; private const string LAST_REF_LAST_VALUE = "new_lastnumber"; #endregion Constant Values /// <summary> /// Initializes a new instance of the <see cref="GenerateOpportunityAutoNumber"/> class. /// </summary> public GenerateOpportunityAutoNumber() : base(typeof(GenerateOpportunityAutoNumber)) { base.RegisteredEvents.Add(new Tuple<int, string, string, Action<LocalPluginContext>>(20, "Create", "opportunity", new Action<LocalPluginContext>(ExecuteGenerateOpportunityAutoNumber))); // Note : you can register for more events here if this plugin is not specific to an individual entity and message combination. // You may also need to update your RegisterFile.crmregister plug-in registration file to reflect any change. } /// <summary> /// Executes the plug-in. /// </summary> /// <param name="localContext">The <see cref="LocalPluginContext"/> which contains the /// <see cref="IPluginExecutionContext"/>, /// <see cref="IOrganizationService"/> /// and <see cref="ITracingService"/> /// </param> /// <remarks> /// For improved performance, Microsoft Dynamics CRM caches plug-in instances. /// The plug-in's Execute method should be written to be stateless as the constructor /// is not called for every invocation of the plug-in. Also, multiple system threads /// could execute the plug-in at the same time. All per invocation state information /// is stored in the context. This means that you should not use global variables in plug-ins. /// </remarks> protected void ExecuteGenerateOpportunityAutoNumber(LocalPluginContext localContext) { if (localContext == null) { throw new ArgumentNullException("localContext"); } IPluginExecutionContext context = localContext.PluginExecutionContext; IOrganizationService service = localContext.OrganizationService; //Can replace with AdminOrganizationService to give admin privileges. if (context.InputParameters.Contains("Target") && context.InputParameters["Target"] is Entity) { Entity entity = (Entity)context.InputParameters["Target"]; if (entity.LogicalName == OPPORTUNITY_ENTITY_NAME) { if (!entity.Attributes.Contains(OPPORTUNITY_REF_ID)) { try { string newId = RetrieveAndUpdateLastId(service); entity.Attributes.Add(OPPORTUNITY_REF_ID, newId); } catch (FaultException ex) { throw new InvalidPluginExecutionException("GenerateOpportunityAutoNumber plugin error: ", ex); //tracingService.Trace("GenerateOpportunityAutoNumber plugin error: {0}", ex.Message); } } } } } /// <summary> /// Lock used to enforce single access to the "Last Identifier" record /// </summary> private static readonly object lastIdentifierLock = new object(); /// <summary> /// </summary> /// <param name="service">Reference to the IOrganizationService</param> private string RetrieveAndUpdateLastId(IOrganizationService service) { lock (lastIdentifierLock) { string result = null; ColumnSet cols = new ColumnSet(); cols.AddColumns(LAST_REF_LAST_VALUE, LAST_REF_PRIMARY_KEY); QueryExpression query = new QueryExpression(); query.ColumnSet = cols; query.EntityName = LAST_REF_ENTITY_NAME; EntityCollection ec = service.RetrieveMultiple(query); if (ec.Entities.Count >= 1) { foreach (Entity identifier in ec.Entities) { if (identifier.Attributes.Contains(LAST_REF_LAST_VALUE)) { int? lastValue = identifier[LAST_REF_LAST_VALUE] as int?; if (lastValue != null) { string newValue = (lastValue.Value + 1).ToString().PadLeft(7, '0'); result = String.Format("SN{0}", newValue); identifier[LAST_REF_LAST_VALUE] = lastValue.Value + 1; service.Update(identifier); //throw new FaultException("Stop here!"); break; } } } } return result; } } } }
This uses the automatically created base class Plugin.cs, which in turn inherits from IPlugin. The code of this is below:
public class Plugin : IPlugin { protected class LocalPluginContext { internal IServiceProvider ServiceProvider { get; private set; } internal IOrganizationService OrganizationService { get; private set; } internal IOrganizationService AdminOrganizationService { get; private set; } internal IPluginExecutionContext PluginExecutionContext { get; private set; } internal ITracingService TracingService { get; private set; } private LocalPluginContext() { } internal LocalPluginContext(IServiceProvider serviceProvider) { if (serviceProvider == null) { throw new ArgumentNullException("serviceProvider"); } // Obtain the execution context service from the service provider. this.PluginExecutionContext = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext)); // Obtain the tracing service from the service provider. this.TracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService)); // Obtain the Organization Service factory service from the service provider IOrganizationServiceFactory factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory)); // Use the factory to generate the Organization Service. this.OrganizationService = factory.CreateOrganizationService(this.PluginExecutionContext.UserId); } internal void Trace(string message) { if (string.IsNullOrWhiteSpace(message) || this.TracingService == null) { return; } if (this.PluginExecutionContext == null) { this.TracingService.Trace(message); } else { this.TracingService.Trace( "{0}, Correlation Id: {1}, Initiating User: {2}", message, this.PluginExecutionContext.CorrelationId, this.PluginExecutionContext.InitiatingUserId); } } } private Collection<Tuple<int, string, string, Action<LocalPluginContext>>> registeredEvents; /// <summary> /// Gets the List of events that the plug-in should fire for. Each List /// Item is a <see cref="System.Tuple"/> containing the Pipeline Stage, Message and (optionally) the Primary Entity. /// In addition, the fourth parameter provide the delegate to invoke on a matching registration. /// </summary> protected Collection<Tuple<int, string, string, Action<LocalPluginContext>>> RegisteredEvents { get { if (this.registeredEvents == null) { this.registeredEvents = new Collection<Tuple<int, string, string, Action<LocalPluginContext>>>(); } return this.registeredEvents; } } /// <summary> /// Gets or sets the name of the child class. /// </summary> /// <value>The name of the child class.</value> protected string ChildClassName { get; private set; } /// <summary> /// Initializes a new instance of the <see cref="Plugin"/> class. /// </summary> /// <param name="childClassName">The <see cref=" cred="Type"/> of the derived class.</param> internal Plugin(Type childClassName) { this.ChildClassName = childClassName.ToString(); } /// <summary> /// Executes the plug-in. /// </summary> /// <param name="serviceProvider">The service provider.</param> /// <remarks> /// For improved performance, Microsoft Dynamics CRM caches plug-in instances. /// The plug-in's Execute method should be written to be stateless as the constructor /// is not called for every invocation of the plug-in. Also, multiple system threads /// could execute the plug-in at the same time. All per invocation state information /// is stored in the context. This means that you should not use global variables in plug-ins. /// </remarks> public void Execute(IServiceProvider serviceProvider) { if (serviceProvider == null) { throw new ArgumentNullException("serviceProvider"); } // Construct the Local plug-in context. LocalPluginContext localcontext = new LocalPluginContext(serviceProvider); localcontext.Trace(string.Format(CultureInfo.InvariantCulture, "Entered {0}.Execute()", this.ChildClassName)); try { // Iterate over all of the expected registered events to ensure that the plugin // has been invoked by an expected event // For any given plug-in event at an instance in time, we would expect at most 1 result to match. Action<LocalPluginContext> entityAction = (from a in this.RegisteredEvents where ( a.Item1 == localcontext.PluginExecutionContext.Stage && a.Item2 == localcontext.PluginExecutionContext.MessageName && (string.IsNullOrWhiteSpace(a.Item3) ? true : a.Item3 == localcontext.PluginExecutionContext.PrimaryEntityName) ) select a.Item4).FirstOrDefault(); if (entityAction != null) { localcontext.Trace(string.Format( CultureInfo.InvariantCulture, "{0} is firing for Entity: {1}, Message: {2}", this.ChildClassName, localcontext.PluginExecutionContext.PrimaryEntityName, localcontext.PluginExecutionContext.MessageName)); entityAction.Invoke(localcontext); // now exit - if the derived plug-in has incorrectly registered overlapping event registrations, // guard against multiple executions. return; } } catch (FaultException<OrganizationServiceFault> e) { localcontext.Trace(string.Format(CultureInfo.InvariantCulture, "Exception: {0}", e.ToString())); // Handle the exception. throw; } finally { localcontext.Trace(string.Format(CultureInfo.InvariantCulture, "Exiting {0}.Execute()", this.ChildClassName)); } } } }
As you can see this creates the IOrganizationService with the PluginExecutionContext.UserId, which I thought would be the impersonating user, but this doesn't seem to happen. Could it be the user I'm trying to impersonate needs to be in the PrivUserGroup if I'm using AD?- Edited by sr28 Friday, July 17, 2015 3:49 PM
Friday, July 17, 2015 3:31 PM