Monday, June 01, 2009

Field Security Level For Microsoft Dynamics CRM 4.0 With Own Hands

Following article describes how to create mechanism which provides/restricts access to see fields of records in edit form/print preview/grids.

Idea:
1. Creation two custom entities - Field Security Level(contains name of entity to restrict access to fields and Business Unit for which restriction will be operate) and Field Security Level Attribute (contains Attribute Name and Bit flag - Access Allowed/Disallowed).
Field Security Level entity is parent to Field Security Level Attribute. Both entities are organization owned.
2. Field Security Level form scripting.
3. Writing a plugin which will create Field Security Level Attributes for just created Field Security Level based on entity name.
4. Writing a base class which will retrieve fields restriction for entity.
5. Writing a plugin which will handle Execute message, retrieve entity name and modify request to prevent appearance of 'forbidden' columns.
6. Writing a plugin which will handle Retrieve message, retrieve entity name and modify request to prevent appearance of 'forbidden' columns.
7. Writing a plugin which will handle RetrieveMultiple message, retrieve entity name and modify request to prevent appearance of 'forbidden' columns.


1.1. Creation of Field Security Level Entity:



Here I input Display Name, Plural Name, ownership is organization, name of entity. Mark all details as shown.



I set primary attribute name as new_entityname.



I create required attributes:
new_bu - Display name of Business Unit
new_buid - identifier of Business Unit
new_entitydisplayname - display name of entity (entity schema name will be stored in new_entityname field)
new_entitytypecode - entity type code

I design the appearance of form:



Iframe I have added for child details display.
Second tab will be hidden. Here is it:



1.2. Child entity creation:



Here I input Display Name, Plural Name, ownership is organization, name of entity.



I set primary attribute name as new_name.





I've added the relationship between Field Security Level and Field Security Level Attribute.



I create required attributes:
new_isallowed - attribute show accessibility of field
new_fieldsecuritylevelid - lookup attribute - to the primary entity Security Level View
new_displayname - display name of attribute



I configure form appearance.

2. Field Security Level OnLoad script:

function ConvertEntityToPicklist(fieldName, dataItems)
{
var defaultValue = crmForm.all[fieldName].DataValue;
var table = crmForm.all[fieldName + "_d"];
var select = "<select req='0' id='" + fieldName + "' name='" + fieldName + "' defaultSelected='' class='ms-crm-SelectBox' tabindex='1170'>";
var defaultValueFound = false;

for (var i = 0; i < dataItems.length; i++)
if (dataItems[i].selectSingleNode('IsCustomizable').text == "true")
{
select += "<option value='" + dataItems[i].selectSingleNode('LogicalName').text + "' ";
select += "entitytypeid='"+dataItems[i].selectSingleNode('ObjectTypeCode').text+"'";
if (dataItems[i].selectSingleNode('LogicalName').text == defaultValue)
{
select += " SELECTED";
defaultValueFound = true;
}
select += ">" + dataItems[i].selectSingleNode('DisplayName/LocLabels/LocLabel/Label').text + "</option>";
}

if ((defaultValue != null) && (defaultValue.length > 0) && !defaultValueFound)
{
select += "<option value='" + defaultValue + "' SELECTED>" + defaultValue + "</option>";
}

select += "</select>";
table.innerHTML = select;
}

function ConvertBUToPicklist(fieldName, dataItems)
{
var defaultValue = crmForm.all.new_buid.DataValue;

if (defaultValue == null)
{
defaultValue = dataItems[0].selectSingleNode('./q1:businessunitid').nodeTypedValue;
crmForm.all.new_buid.DataValue = defaultValue
}


var table = crmForm.all[fieldName + "_d"];
var select = "<select req='0' id='" + fieldName + "' name='" + fieldName + "' defaultSelected='' class='ms-crm-SelectBox' tabindex='1170'>";
var defaultValueFound = false;

for (var i = 0; i < dataItems.length; i++)
{
select += "<option value='" + dataItems[i].selectSingleNode('./q1:businessunitid').nodeTypedValue + "'";
if (dataItems[i].selectSingleNode('./q1:businessunitid').nodeTypedValue == defaultValue)
{
select += " SELECTED";
defaultValueFound = true;
}
select += ">" + dataItems[i].selectSingleNode('./q1:name').nodeTypedValue + "</option>";
}

if ((defaultValue != null) && (defaultValue.length > 0) && !defaultValueFound)
{
select += "<option value='" + defaultValue + "' SELECTED>" + defaultValue + "</option>";
}

select += "</select>";
table.innerHTML = select;
}

crmForm.all.tab1Tab.style.display = 'none';

var request = "" +
"<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">" +
GenerateAuthenticationHeader() +
" <soap:Body>" +
" <Execute xmlns=\"http://schemas.microsoft.com/crm/2007/WebServices\">" +
" <Request xsi:type=\"RetrieveAllEntitiesRequest\">" +
" <RetrieveAsIfPublished>true</RetrieveAsIfPublished>" +
" <MetadataItems>EntitiesOnly</MetadataItems>" +
" </Request>" +
" </Execute>" +
" </soap:Body>" +
"</soap:Envelope>";

var xmlHttpRequest = new ActiveXObject("Msxml2.XMLHTTP");

xmlHttpRequest.Open("POST", "/mscrmservices/2007/MetadataService.asmx", false);
xmlHttpRequest.setRequestHeader("SOAPAction","http://schemas.microsoft.com/crm/2007/WebServices/Execute");
xmlHttpRequest.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
xmlHttpRequest.setRequestHeader("Content-Length", request.length);
xmlHttpRequest.send(request);

var result = xmlHttpRequest.responseXML;
var schemaNames = result.selectNodes("//CrmMetadata/CrmMetadata");
ConvertEntityToPicklist('new_entityname', schemaNames);



request = "<?xml version='1.0' encoding='utf-8'?>"+
"<soap:Envelope xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/'"+
" xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'"+
" xmlns:xsd='http://www.w3.org/2001/XMLSchema'>"+
GenerateAuthenticationHeader() +
"<soap:Body>"+
"<RetrieveMultiple xmlns='http://schemas.microsoft.com/crm/2007/WebServices'>"+
"<query xmlns:q1='http://schemas.microsoft.com/crm/2006/Query'"+
" xsi:type='q1:QueryExpression'>"+
"<q1:EntityName>businessunit</q1:EntityName>"+
"<q1:ColumnSet xsi:type='q1:ColumnSet'>"+
"<q1:Attributes>"+
"<q1:Attribute>name</q1:Attribute>"+
"</q1:Attributes>"+
"</q1:ColumnSet>"+
"<q1:Distinct>false</q1:Distinct>"+
"</query>"+
"</RetrieveMultiple>"+
"</soap:Body>"+
"</soap:Envelope>";

xmlHttpRequest = new ActiveXObject("Msxml2.XMLHTTP");

xmlHttpRequest.Open("POST", "/mscrmservices/2007/CrmService.asmx", false);
xmlHttpRequest.setRequestHeader("SOAPAction","http://schemas.microsoft.com/crm/2007/WebServices/RetrieveMultiple");
xmlHttpRequest.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
xmlHttpRequest.setRequestHeader("Content-Length", request.length);
xmlHttpRequest.send(request);

result = xmlHttpRequest.responseXML;

var BUs = result.getElementsByTagName('BusinessEntity');
ConvertBUToPicklist('new_buid', BUs);

if (crmForm.FormType != 1)
{
document.getElementById('new_entityname_d').disabled = true;
document.getElementById('new_buid_d').disabled = true;

var url = "areas.aspx?";
url += "oId=" + crmFormSubmit.crmFormSubmitId.value;
url += "&oType=" + crmFormSubmit.crmFormSubmitObjectType.value;
url += "&security=" + crmFormSubmit.crmFormSubmitSecurity.value;
url += "&tabSet=new_new_fieldlevelsecurity";
crmForm.all.IFRAME_attributes.src = url;

crmForm.all.IFRAME_attributes.attachEvent("onreadystatechange", Ready);

function Ready()
{
var doc = crmForm.all.IFRAME_attributes.contentWindow.document;
if (doc.getElementById("mnuBar1") != null)
doc.getElementById("mnuBar1").style.display = "none";
}

}


Field Security Leve OnSave script:

if (crmForm.FormType == 1)
{
crmForm.all.new_bu.DataValue = document.getElementById('new_buid').options[document.getElementById('new_buid').selectedIndex].innerText;

crmForm.all.new_entitydisplayname.DataValue = document.getElementById('new_entityname').options[document.getElementById('new_entityname').selectedIndex].innerText;

crmForm.all.new_entitytypecode.DataValue = document.getElementById('new_entityname').options[document.getElementById('new_entityname').selectedIndex].getAttribute('entitytypeid');
}


3. Plugin for creation Child entities (attributes) for field security level. Step must be registered as a Post Syncronous Create to new_fieldsecuritylevel entity. Code of the plugin:

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Crm.Sdk;
using Microsoft.Crm.SdkTypeProxy;
using Microsoft.Crm.Sdk.Metadata;
using Microsoft.Crm.SdkTypeProxy.Metadata;

namespace FieldSecurity
{
public class FieldSecurityCreateHandler : IPlugin
{

#region CTOR

public FieldSecurityCreateHandler(string config, string secureConfig)
{
}

#endregion CTOR


#region IPlugin Members

public void Execute(IPluginExecutionContext context)
{
try
{
if (context.MessageName == MessageName.Create &&
context.InputParameters.Contains("Target") &&
context.InputParameters["Target"] is DynamicEntity &&
((DynamicEntity)context.InputParameters["Target"]).Name == "new_fieldlevelsecurity")
{
DynamicEntity fls = (DynamicEntity)context.InputParameters["Target"];
string entityname = (string)fls["new_entityname"];
Guid recordid = (Guid)context.OutputParameters["Id"];

//Metadata sevice instance creation
IMetadataService mservice = context.CreateMetadataService(true);

//Request for entity attributes creation
RetrieveEntityRequest request = new RetrieveEntityRequest();
request.EntityItems = EntityItems.IncludeAttributes;
request.LogicalName = entityname;
request.RetrieveAsIfPublished = true;

RetrieveEntityResponse response = (RetrieveEntityResponse)mservice.Execute(request);

//CrmService instance creation
ICrmService crmservice = context.CreateCrmService(true);

foreach (AttributeMetadata am in response.EntityMetadata.Attributes)
if (am.ValidForRead.Value == true &&
am.ValidForUpdate.Value == true &&
string.IsNullOrEmpty(am.AttributeOf) && //remove attributes live customeridtype
string.IsNullOrEmpty(am.AggregateOf) &&
am.LogicalName != "timezoneruleversionnumber" &&
am.LogicalName != "utcconversiontimezonecode" &&
am.AttributeType.Value != AttributeType.PrimaryKey)//remove primary key attribute
{
//Create child entity which contains information about
//attribute and restriction (show or not)
DynamicEntity attributeEntity = new DynamicEntity("new_attribute");
attributeEntity["new_name"] = am.LogicalName;
attributeEntity["new_displayname"] = am.DisplayName.LocLabels.Length == 0 ? am.LogicalName : am.DisplayName.LocLabels[0].Label;
attributeEntity["new_isallowed"] = new CrmBoolean(true);
attributeEntity["new_fieldlevelsecurityid"] = new Lookup("new_fieldlevelsecurity", recordid);

crmservice.Create(attributeEntity);
}
}
}
catch (System.Web.Services.Protocols.SoapException exc)
{
throw new Exception(exc.Detail.InnerXml);
}
}

#endregion
}
}


4. Writing a base class which will retrieve fields restriction for entity.

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Crm.Sdk;
using Microsoft.Crm.SdkTypeProxy;
using Microsoft.Crm.Sdk.Query;

namespace FieldSecurity
{
public abstract class BasePlugin
{
protected List<string> RetrieveRestrictions(IPluginExecutionContext context, string entityname)
{
if (entityname == "new_attribute")
return new List<string>();

ICrmService crmService = context.CreateCrmService(true);

systemuser user = (systemuser)crmService.Retrieve(EntityName.systemuser.ToString(), context.UserId, new ColumnSet(new string[] { "businessunitid" }));
Guid buid = user.businessunitid.Value;

QueryExpression query = new QueryExpression("new_attribute");
query.ColumnSet = new ColumnSet(new string[] { "new_name" });
query.Criteria.AddCondition("new_isallowed", ConditionOperator.Equal, false);

LinkEntity link = query.AddLink("new_fieldlevelsecurity", "new_fieldlevelsecurityid", "new_fieldlevelsecurityid");
link.LinkCriteria.AddCondition("new_entityname", ConditionOperator.Equal, entityname);
link.LinkCriteria.AddCondition("new_buid", ConditionOperator.Equal, "{" + buid.ToString().ToUpper() + "}");

RetrieveMultipleRequest request = new RetrieveMultipleRequest();
request.Query = query;
request.ReturnDynamicEntities = true;

List<BusinessEntity> restrictions = ((RetrieveMultipleResponse)crmService.Execute(request)).BusinessEntityCollection.BusinessEntities;

List<string> result = new List<string>();

foreach (DynamicEntity entity in restrictions)
result.Add((string)entity["new_name"]);

return result;
}
}
}


5. Writing a plugin which will handle Execute message, retrieve entity name and modify request to prevent appearance of 'forbidden' columns. Step must be registered as a Pre Syncronous on message Execute with no Primary and Child entity selection.

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Crm.Sdk;
using System.Xml;

namespace FieldSecurity
{
public class ExecuteHandler : BasePlugin, IPlugin
{

#region CTOR

public ExecuteHandler(string config, string secureConfig)
{
}

#endregion CTOR

#region IPlugin Members

public void Execute(IPluginExecutionContext context)
{
if (context.MessageName == "Execute" && context.InputParameters.Contains("FetchXml"))
{
XmlDocument indoc = new XmlDocument();
indoc.LoadXml((string)context.InputParameters["FetchXml"]);

string entityName = indoc.SelectSingleNode("//fetch/entity").Attributes["name"].InnerText;

List<string> restrictions = RetrieveRestrictions(context, entityName);

if (restrictions.Count == 0)
return;

foreach (XmlNode node in indoc.SelectNodes("//fetch/entity/attribute"))
if (restrictions.Contains(node.Attributes["name"].Value))
node.ParentNode.RemoveChild(node);

context.InputParameters["FetchXml"] = indoc.OuterXml;
}
}

#endregion IPlugin Members
}
}


6. Writing a plugin which will handle Retrieve message, retrieve entity name and modify request to prevent appearance of 'forbidden' columns. Step must be registered as a Pre Syncronous on message Retrieve with no Primary and Child entity selection.

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Crm.Sdk;
using Microsoft.Crm.Sdk.Query;
using Microsoft.Crm.SdkTypeProxy;

namespace FieldSecurity
{
public class RetrieveHandler : BasePlugin, IPlugin
{

#region CTOR

public RetrieveHandler(string config, string secureConfig)
{
}

#endregion CTOR

#region IPlugin Members

public void Execute(IPluginExecutionContext context)
{
if (context.MessageName == MessageName.Retrieve)
{
string entityName = context.PrimaryEntityName;

List<string> restrictions = RetrieveRestrictions(context, entityName);

if (restrictions.Count == 0)
return;

ColumnSet columns = (ColumnSet)context.InputParameters["ColumnSet"];

foreach (string restriction in restrictions)
while (columns.Attributes.Contains(restriction))
columns.RemoveColumn(restriction);

context.InputParameters["ColumnSet"] = columns;
}
}

#endregion IPlugin Members
}
}


7. Writing a plugin which will handle RetrieveMultiple message, retrieve entity name and modify request to prevent appearance of 'forbidden' columns. Step must be registered as a Pre Syncronous on message Retrieve with no Primary and Child entity selection.

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Crm.Sdk;
using Microsoft.Crm.Sdk.Query;

namespace FieldSecurity
{
public class RetrieveMultipleHandler : BasePlugin, IPlugin
{

#region CTOR

public RetrieveMultipleHandler(string config, string secureConfig)
{
}

#endregion CTOR

#region IPlugin Members

public void Execute(IPluginExecutionContext context)
{
if (context.MessageName == MessageName.RetrieveMultiple)
{
string entityName = context.PrimaryEntityName;

List<string> restrictions = RetrieveRestrictions(context, entityName);

if (restrictions.Count == 0)
return;

QueryExpression query = (QueryExpression)context.InputParameters["Query"];

foreach (string restriction in restrictions)
while (((ColumnSet)query.ColumnSet).Attributes.Contains(restriction))
((ColumnSet)query.ColumnSet).RemoveColumn(restriction);

context.InputParameters["Query"] = query;
}
}

#endregion IPlugin Members
}
}


And Video Demonstration of functionality: