How to audit Privileges changes (Security Role) on MS CRM 4.0

One of the challenges that I face while I was working on auditing changes on the major entities at MS CRM 4.0 is to audit changes on Privileges… CRM 4.0 consider privilege entity as a special case this is why we need to build a special case for auditing changes here.

First of all, there is no supported Message (Event) to handle this case. So we have to enable couple of messages to be able to audit changes on security Roles. Those messages called:

1. AddPrivileges. –> this event will handle the creating of a new Security Role

2.  ReplacePrivileges. –> this message will handle adding/update/delete security privileges

The below script help you to enable those messages:

 

/*

Enable Privileges unsupported messages

*/

DECLARE @Message1   uniqueidentifier

DECLARE @Message2 uniqueidentifier

SET @Message1= (SELECT SdkMessageId FROM SdkMessageBase WHERE [Name] = ‘ReplacePrivileges’)

SET @Message2 = (SELECT SdkMessageId FROM SdkMessageBase WHERE [Name] = ‘AddPrivileges’)

UPDATE SdkMessageFilterBase SET IsCustomProcessingStepAllowed = 1

WHERE SdkMessageId = @Message1

UPDATE SdkMessageFilterBase SET IsCustomProcessingStepAllowed = 1

WHERE SdkMessageId = @Message2

After applying the above data script to MSCRM DB, you will be able to find those messages exposed for you on MS CRM 4.0 Plugin Registration tool so you will be able to register a plugin against those messages (Events). Now we need to build the Message handler to audit any changes, below steps showing you how to do that.

I am going to use the same code that audit regular entities with a little adjustments, the reasons of my adjustments are:

· The Pre-Image and Post-Image will not have enough info that needed by Audit handlers; thus I need to add a special handler for that.

· The Audit code that you can download from internet does not handle ReplacePrivileges/AddPrivileges Messages, thus we need to build a special case for that.

Note: To read more about default Audit component please refer to this article: "Auditing MS CRM 4.0 Record Changes"

To solve the problem (A), I have to add a Pre Handler for ReplacePrivilege Message, this is a very simple handler that will contains code to store the current Privileges (before doing update) on a shared Variable to be passed to the Post ReplacePrivilege message handler; below is the code of PreReplacePrivilege Message handler:

public class PreReplacePrivilege:IPlugin

{

#region IPlugin Members

public void Execute(IPluginExecutionContext context)

{

if (context.MessageName == "ReplacePrivileges")

{

if (context.InputParameters.Contains("RoleId"))

{

context.SharedVariables["PreRolePrivilagesValues"] =

Utilities.GetRolePrivilages(context.InputParameters.Properties["RoleId"].ToString(),

context.CreateCrmService(true));

context.SharedVariables["RoleId"] = context.InputParameters.Properties["RoleId"];

}

}

}

#endregion

}

static class Utilities

{

/// <summary>

/// Get privilages for Roles.

/// </summary>

public static Dictionary<string, string> GetRolePrivilages(string roleId, ICrmService crmService)

{

Microsoft.Crm.Sdk.Query.AllColumns cols = new Microsoft.Crm.Sdk.Query.AllColumns();

Dictionary<Guid, string> privileges = GetAllPrivilages(crmService); //Get list of all privileges

RetrieveRolePrivilegesRoleRequest request = new RetrieveRolePrivilegesRoleRequest();

request.RoleId = new Guid(roleId);

RetrieveRolePrivilegesRoleResponse response = (RetrieveRolePrivilegesRoleResponse)crmService.Execute(request);

Dictionary<string, string> rolePrigivleges = ResolvePrivilageNames(privileges, response.RolePrivileges);

return rolePrigivleges;

}

/// <summary>

/// Get all privilages on system

/// </summary>

/// <param name="crmService"></param>

/// <returns></returns>

private static Dictionary<Guid, string> GetAllPrivilages(ICrmService crmService)

{

Dictionary<Guid, string> privilages = new Dictionary<Guid, string>();

RetrievePrivilegeSetRequest requestp = new RetrievePrivilegeSetRequest();

requestp.ReturnDynamicEntities = true;

RetrievePrivilegeSetResponse responsep = (RetrievePrivilegeSetResponse)crmService.Execute(requestp);

foreach (DynamicEntity p in responsep.BusinessEntityCollection.BusinessEntities)

{

privilages.Add( ((Key)p["privilegeid"]).Value,p["name"].ToString());

}

return privilages;

}

/// <summary>

/// Resolving Privileges Names

/// </summary>

/// <param name="rolePrivilegesList"></param>

/// <param name="crmService"></param>

/// <returns></returns>

public static Dictionary<string, string> ResolvePrivilageNames(RolePrivilege[] rolePrivilegesList, ICrmServicecrmService)

{

Dictionary<Guid, string> privileges = GetAllPrivilages(crmService);

Dictionary<string, string> rolePrigivleges = new Dictionary<string, string>();

foreach (RolePrivilege r in rolePrivilegesList)

{

rolePrigivleges.Add(privileges[r.PrivilegeId], r.Depth.ToString());

}

foreach (KeyValuePair<Guid, string> pair in privileges)

{

if (!rolePrigivleges.ContainsKey(pair.Value))

rolePrigivleges.Add(pair.Value, "None");

}

return rolePrigivleges;

}

public static string GetPropertyValue(object property)

{

try

{

if (property.GetType() == typeof(CrmBoolean))

return ((CrmBoolean)property).Value.ToString();

else if (property.GetType() == typeof(CrmDateTime))

return ((CrmDateTime)property).UserTime.ToString();

else if (property.GetType() == typeof(Owner))

return ((Owner)property).Value.ToString();

else if (property.GetType() == typeof(Lookup))

return ((Lookup)property).Value.ToString();

else if (property.GetType() == typeof(Picklist))

return ((Picklist)property).Value.ToString();

else if (property.GetType() == typeof(StringProperty))

return ((StringProperty)property).Value.ToString();

else if (property.GetType() == typeof(LookupProperty))

return ((LookupProperty)property).Value.name.ToString();

else if (property.GetType() == typeof(OwnerProperty))

return ((OwnerProperty)property).Value.name.ToString();

else if (property.GetType() == typeof(PicklistProperty))

return ((PicklistProperty)property).Value.name.ToString();

else if (property.GetType() == typeof(CrmDateTimeProperty))

return ((CrmDateTimeProperty)property).Value.UserTime.ToString();

else if (property.GetType() == typeof(KeyProperty))

return ((KeyProperty)property).Value.Value.ToString();

else if (property.GetType() == typeof(CrmBooleanProperty))

return ((CrmBooleanProperty)property).Value.Value.ToString();

else if (property.GetType() == typeof(CrmDecimalProperty))

return ((CrmDecimalProperty)property).Value.Value.ToString();

else if (property.GetType() == typeof(CrmFloatProperty))

return ((CrmFloatProperty)property).Value.Value.ToString();

else if (property.GetType() == typeof(CrmMoneyProperty))

return ((CrmMoneyProperty)property).Value.Value.ToString();

else if (property.GetType() == typeof(CrmNumberProperty))

return ((CrmNumberProperty)property).Value.Value.ToString();

else if (property.GetType() == typeof(CustomerProperty))

return ((CustomerProperty)property).Value.name.ToString();

else if (property.GetType() == typeof(CustomerProperty))

return ((CustomerProperty)property).Value.name.ToString();

else if (property.GetType() == typeof(StatusProperty))

return ((StatusProperty)property).Value.name.ToString();

else if (property.GetType() == typeof(StateProperty))

return ((StateProperty)property).Value.ToString();

else if (property.GetType() == typeof(UniqueIdentifierProperty))

return ((UniqueIdentifierProperty)property).Value.Value.ToString();

else if (property == null)

return "";

else

return property.ToString();

}

catch

{ return ""; }

}

public static AuditDifferenceCollection AddDifferences(IMetadataService metaService, string entityName,DynamicEntity preImage, DynamicEntity postImage)

{

AuditDifferenceCollection col = new AuditDifferenceCollection();

System.Collections.Hashtable attributesMetadata =

MetadataHelper.GetEntityAttributeMetadata(metaService, entityName, Microsoft.Crm.Sdk.Metadata.EntityItems.IncludeAttributes);

//For any Message that has a Pre and/or Post Image. I will add the values to the Audit Differences.

foreach (Property prop in preImage.Properties)

{

if(prop.GetType()!= typeof(KeyProperty) && prop.Name!="modifiedby" && prop.Name != "modifiedon" && prop.Name != "createdby" && prop.Name != "createdon")

{

col.Add(new AuditDifference(prop.Name,attributesMetadata[prop.Name].ToString(),Utilities.GetPropertyValue(prop), ""));

}

}

foreach (Property prop in postImage.Properties)

{

if(prop.GetType()!= typeof(KeyProperty) && prop.Name!="modifiedby" && prop.Name != "modifiedon" && prop.Name != "createdby" && prop.Name != "createdon")

{

if (col.Contains(prop.Name))

{

AuditDifference diff = col[prop.Name];

diff.CurrentValue = Utilities.GetPropertyValue(prop);

col[prop.Name] = diff;

}

else

{

col.Add(new AuditDifference(prop.Name, attributesMetadata[prop.Name].ToString(),"",Utilities.GetPropertyValue(prop)));

}

}

}

return col;

}

public static AuditDifferenceCollection AddDifferences(IMetadataService metaService, string entityName,Dictionary<string, string> preValues, Dictionary<string, string> postValues)

{

AuditDifferenceCollection col = new AuditDifferenceCollection();

if (preValues != null && preValues.Count > 0)

{

foreach (KeyValuePair<string, string> entry in preValues)

{

col.Add(new AuditDifference(entry.Key + ":" + entry.Value, entry.Value, ""));

}

}

if (postValues != null & postValues.Count > 0)

{

foreach (KeyValuePair<string, string> entry in postValues)

{

if (col.Contains(entry.Key + ":" + entry.Value))

{

AuditDifference diff = col[entry.Key + ":" + entry.Value];

diff.CurrentValue = entry.Value;

col[entry.Key + ":" + entry.Value] = diff;

}

else

{

col.Add(new AuditDifference(entry.Key + ":" + entry.Value, "", entry.Value));

}

}

}

return col;

}

public static string AddAttributeToList(string value, string list)

{

if (list.IndexOf(value) == -1)

{

if (list == "")

list = value;

else

 

===================================

To solve problem (B) we need to adjust the Execute Method of  Create Class… this is the full code after adjustment for Create class:

==================================

public class Create : IPlugin

{

string m_config;

string m_secureConfig;

private string _relationshipAttribute = "";

private string _primaryAttribute = "";

public string Config

{

get { return m_config; }

set { m_config = value; }

}

public string SecureConfig

{

get { return m_secureConfig; }

set { m_secureConfig = value; }

}

public Create(string config, string secureConfig)

{

m_config = config;

m_secureConfig = secureConfig;

if (config.Trim() != "")

{

string[] configSettings = config.Split(‘;’);

for (int i = 0; i < configSettings.Length; i++)

{

string[] values = configSettings[i].Split(‘=’);

switch (values[0].ToString().ToLower())

{

case "primaryattribute":

_primaryAttribute = values[1].ToString().Trim().ToLower();

break;

case "relationshipattribute":

_relationshipAttribute = values[1].ToString().Trim().ToLower();

break;

}

}

}

}

public void Execute(IPluginExecutionContext context)

{

string attributes = "";

Audit audit = new Audit();

DynamicEntity PreImage = new DynamicEntity();

DynamicEntity PostImage = new DynamicEntity();

audit.Service = context.CreateCrmService(true);

audit.MetadataService = context.CreateMetadataService(true);

audit.EntityName = context.PrimaryEntityName;

audit.Type = context.MessageName;

audit.Name = context.PrimaryEntityName + " " + context.MessageName;

Dictionary<String, String> preValues = new Dictionary<string, string>();

Dictionary<String, String> postValues = new Dictionary<string, string>();

if (context.InputParameters.Properties.Contains("Target") &&

context.InputParameters.Properties["Target"] is Moniker)

{

Moniker moniker = (Moniker)context.InputParameters.Properties["Target"];

audit.RecordID = moniker.Id.ToString();

}

else if (context.InputParameters.Properties.Contains("EntityMoniker") &&

context.InputParameters.Properties["EntityMoniker"] is Moniker)

{

Moniker moniker = (Moniker)context.InputParameters.Properties["EntityMoniker"];

audit.RecordID = moniker.Id.ToString();

}

else if (context.InputParameters.Properties.Contains("Target") &&

context.InputParameters.Properties["Target"] is DynamicEntity)

{

//if the transaction is create then get the ID from the OutputParameters

if (context.OutputParameters.Contains("id"))

audit.RecordID = context.OutputParameters.Properties["id"].ToString();

//pull the KeyProperty from the entity.

foreach (Property prop in ((DynamicEntity)context.InputParameters.Properties["Target"]).Properties)

{

if (prop.GetType() == typeof(KeyProperty))

{

audit.RecordID = ((KeyProperty)prop).Value.Value.ToString();

break;

}

}

}

else if (context.MessageName == "ReplacePrivileges" ||

context.MessageName == "AddPrivileges" ||

context.MessageName == "RemovePrivilege") //Unsupported Message and it needs special handling

{

if(context.InputParameters.Properties.Contains("RoleId"))

audit.RecordID = ((Guid)context.InputParameters.Properties["RoleId"]).ToString();

else if(context.SharedVariables.Contains("RoleId"))

audit.RecordID = ((Guid) context.SharedVariables["RoleId"]).ToString();

//Pull Pre state of previleges from shared variable populated on PreStage plugin

if (context.SharedVariables.Contains("PreRolePrivilagesValues"))

preValues = (Dictionary<string, string>)context.SharedVariables["PreRolePrivilagesValues"];

else

preValues = new Dictionary<string, string>();

if (context.InputParameters.Contains("Privileges"))

{

postValues = Utilities.ResolvePrivilageNames(

(RolePrivilege[])context.InputParameters["Privileges"], audit.Service);

}

else

postValues = new Dictionary<string, string>();

}

//Get the Pre and Post Images

if (context.PreEntityImages.Properties.Contains("Images") && context.PreEntityImages.Properties["Images"] isDynamicEntity)

PreImage = (DynamicEntity)context.PreEntityImages.Properties["Images"];

if (context.PostEntityImages.Properties.Contains("Images") && context.PostEntityImages.Properties["Images"] isDynamicEntity)

PostImage = (DynamicEntity)context.PostEntityImages.Properties["Images"];

//Compare the images values.

//Messages not support Pre/Post images.

if (string.IsNullOrEmpty(PreImage.Name) && string.IsNullOrEmpty(PostImage.Name))

{

audit.AuditDifferences = Utilities.AddDifferences(audit.MetadataService, context.PrimaryEntityName, preValues, postValues);

}

else //Messages support Pre/Post images.

{

audit.AuditDifferences = Utilities.AddDifferences(audit.MetadataService, context.PrimaryEntityName, PreImage, PostImage);

}

foreach (AuditDifference diff in audit.AuditDifferences)

{

if (context.MessageName == "AddPrivileges" || context.MessageName == "ReplacePrivileges")

diff.ReplaceAttributeName = string.Format("Privilege:{0}", diff.AttributeName.Substring(0, diff.AttributeName.IndexOf(":")));

if (diff.CurrentValue != diff.PreviousValue)

attributes = Utilities.AddAttributeToList(diff.AttributeDisplayName, attributes);

}

if (_relationshipAttribute.Trim() != "" && audit.RecordID.Trim() != "" && context.MessageName != "Delete")

audit.Relationship = new EntityRelationship(_relationshipAttribute, new Guid(audit.RecordID));

audit.Attributes = attributes;

if (!context.PreEntityImages.Properties.Contains("Images")

&& !context.PostEntityImages.Properties.Contains("Images")

&& context.MessageName != "ReplacePrivileges")

{

audit.Create();

}

else

{

if (attributes.Trim() != "")

audit.Create();

}

}

I am attaching below the  3 classes I made changes on, I got the original classes and do the changes to them.

I hope you will have this useful and will solve your issue

 

Download Code