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:
Hi a33ik,
ReplyDeletenice idea. Personally I might change the optic. If user is not allowed to see the information he/she should see "not allowed to view info" or something like it. Otherwise he/she would modify for example the email-address.
Hi, Carsten.
ReplyDeleteAnother one plugin for pre-create|pre-update can be written which will disallow user to change restricted fields.
Yeah. It looks good.
ReplyDeleteAmol
www.mscrmkb.blogspot.com
Very nice post, does it work for Advanced Find as well?
ReplyDeleteYes, Jim. It works with advanced find.
ReplyDeleteHow to hide from Reports? User can always create report to list all data and fields using report wizard.
ReplyDeleteYes. I know this issue...
ReplyDeleteThis is a great solution!!
ReplyDeleteBesides the fields being displayed in the custom reports, I'm having an error when trying to merge two records with a restricted field:
Unable to cast object of type "Microsoft.Crm.Sdk.AllColumns" to type "Microsoft.Crm.Sdk.Query.ColumnSet".
Thank you for your bug report. I'll fix this issue and update the post!
ReplyDeleteThanks for the message on Linkedin. This is a great tool, however I need the security to work for Reports and the infamous print button.
ReplyDeleteThis solution works with infamous print button. For the reports you have to redesign your reports...
ReplyDeleteHi,
ReplyDeleteI tried following the approach above and I ran into the following issues:
1. Business unit is not rendered as a dropdown
2. The grid (with a list of attributes) are not selectable !!
Any help is very much appreciated.
If possible, could please provide the customizations and the source code as a downloadable package.
Cheers,
Kiran Banda
Hi azzik,
ReplyDeleteThis is really a nice post..It works like a charm..Thank you for posting this wonderful article..Its really useful.
Regards
Arun
Hi azzik,
ReplyDeleteIs it possible to hide the attribute while retriving the entities..Here u have remove the data from that particular attribute, but is it possible to hide that particular attribute..
Regards
Arun
Hi, Arun. It seems to be possible with JavaScript. This can be done with writting universal javascript and pasting it to all required Forms OnLoad handlers.
ReplyDeleteHave you seen c360 solution?
Hi azzik,
ReplyDeleteI have seen the screen shot for that c360 solutions..But i cant able to open the download page..Is the source code is given for that product?
How should i write the universal javascript and pasting it to all required Forms OnLoad handlers.Can you please explain me, how to solve this?
Regards
Arun
Hi, Arun.
ReplyDeleteC360 field security level is commercial product so I don't think that they will give you source code...
About universal JavaScript code - you can write code based on CRM WebServices reading dissallowed fields from entities and hide them based on condition this field allowed/disallowed for user.
hi a33ik Butenko ;
ReplyDeletei tried run data audit manager but it resulted error. please help me ?if possible , could please provide the customizations and packable source code with solution ?
hi azzik,
ReplyDeleteRegarding the universal JavaScript, where should i place that file.i have to write it in separate file or i can include in the class file as you have given above.But is there any possibility to call the javascript from class file..
Regards
Arun
Hi, Arun.
ReplyDeleteYou can't use C# class file. This file must be written on JavaScript.
Hi, Crackbyte.
ReplyDeleteFiles with customization and C# files can be downloaded here - http://cid-f63e4bcd4a7f64a4.skydrive.live.com/self.aspx/BlogFiles/DataAudit.zip
Hi azzik,
ReplyDeleteIf i have that separate file, then from where should i call that file.Should i have to call that in every forms in the onLoadevent?
As per your above code, we can use the "Retrive" and "RetriveMultiple" events while loading.can you please tell me, how can i solve this?..
Is it possible to call, javascript from the class file?.
i have gone through one article
from the following link
http://bytes.com/topic/c-sharp/answers/615505-calling-javascript-function-csharp-class
Here they mentioned that, we have to create a javascript file as a package and we can call it in the class file..i have tried and didnt get the solution.if possible can you try this and reply me please..
Regards
Arun
Dear azzik,
ReplyDeleteHave you tried as i said in the previous blog..
Can you tell me, the retrive event is same as onload event.As you have mentioned in your above code(ie. register the plugin in "Retrive" and "RetriveMultiple").will the same process take it for Onload event?
Regards
Arun
Hi, Arun.
ReplyDeleteThis is different events. Handling on Retrieve message is executed on the server side. OnLoad event handlers triggers on client side.
Hi azzik,
ReplyDeleteThank you for your reply.As you have mentioned earlier in our discussion, can you tell me about the universal JavaScript where i have to write the code?
Regards
Arun
You have to develop JavaScript which will read restricted fields and hide them and place on all forms you want to use it.
ReplyDeletewhat type of new_entityname attribute? picklist or nvarchar?
ReplyDeleteinstead should be the primary attribute is nvarchar.. sorry if i'm wrong :)
Yes, it it nvarchar. But it is converted to picklist using JavaScript.
ReplyDeleteI have been getting this error once going into the Field Level Security entity and still can't figure out why I'm exactly getting it:
ReplyDeleteThere was an error with this field's customized event.
Field:window
Event:onload
Error:'crmForm.all.IFRAME_attributes' is null or not an object
Any help would be appreciated.
Thanks!
Have you added iframe with name IFRAME_attributes?
ReplyDeleteI have since. I am a little new to MSCRM so sometimes I have to double check some things. However, I have a new issue. Instead of showing the child form in the iframe, it is only showing a view instead with Attribute Name and Created On instead of the Is Allowed field.
ReplyDeleteYou have to change Associated View of new_attribute entity (child records).
ReplyDeleteVery nice post.
ReplyDeleteA question though:
This works on the output of advanced find, as you cannot see the data in the fields.
But you can still perform a search on a column you aren't allowed to see data from.
Can this be prevented as well?
Hi, Dieter.
ReplyDeleteIt seems that this is possible. Another one pre-Execute and pre-RetrieveMultiple plugin is required to develop.
As explained in this blog, I am completed all the steps. But seems some thing wrong with plugins or customizations. Can you please provide me the customizations and plugin code in zip file. This is really very urgent in my project. Requesting again please let me know the path where can I download them?
ReplyDeleteError:-
ReplyDeleteTry this action again. If the problem continues, check the Microsoft Dynamics CRM Community for solutions or contact your organization's Microsoft Dynamics CRM Administrator. Finally, you can contact Microsoft Support.
When we use first plug-in for which will create Field Security Level Attributes for just created Field Security Level based on entity name.
Please Help me
Regards,
Sandeep Verma
Hello, Sandeep.
ReplyDeleteTry to debug it. This customization and plugin worked fine for me.
I Andriy,
ReplyDeletethanks for this post.
Do you know if this kind of development is possible on 2011 version of MSCRM ?
I am actually not entirely satisfied with the Field Level Security feature given by Microsoft.
I am looking for a solution to implement field level security on out of box fields (like emails, phone numbers etc...) and based on the business unit of the user connected.
Thanks in advance
Hello Pierre,
ReplyDeleteIt would be even easier to implement the same features for CRM 2011 because you will need to implement only handling of RetrieveMultiple message.
This comment has been removed by the author.
ReplyDeleteGreat ! we will try it then
ReplyDelete