Tuesday 29 September 2020

Field Security Profile - Based on Owner

 Recently received requirement related to Field security profile.


Expectation: -

1.     Set to users need access of secure attributes.

2.     Owner of record need access of secure attributes.

3.      Access of secure attributes need to change once ownership of record changed.

4.     Share Access of secure attributes once record is shared.

Solution: - 

As per out -of-box offer from Dynamics CRM only first requirement is feasible but for 2,3 and 4 requirements need to go for customization.

Dynamics CRM provide option to share secure attribute permission via code.

Create a custom workflow to share attribute and register it.

using Microsoft.Xrm.Sdk;

using Microsoft.Xrm.Sdk.Client;

using Microsoft.Xrm.Sdk.Messages;

using Microsoft.Xrm.Sdk.Query;

using Microsoft.Xrm.Sdk.Workflow;

using System;

using System.Activities;

 namespace CustomWorkflows

{

    public sealed class ShareSecuredField : CodeActivity

    {

        [RequiredArgument]

        [Input("Attributes Name")]

        public InArgument<String> AttributeNames { get; set; }

        [Input("Share With User")]

        [ReferenceTarget("systemuser")]

        public InArgument<EntityReference> UserToShare { get; set; }

        [Input("Share With Team")]

        [ReferenceTarget("team")]

        public InArgument<EntityReference> TeamToShare { get; set; }

        [RequiredArgument]

        [Input("Allow Read")]

        [Default("true")]

        public InArgument<Boolean> AllowRead { get; set; }

        [RequiredArgument]

        [Input("Allow Update")]

        [Default("true")]

        public InArgument<Boolean> AllowUpdate { get; set; }

        /// <summary>

        /// Executes the workflow activity.

        /// </summary>

        /// <param name="executionContext">The execution context.</param>

        protected override void Execute(CodeActivityContext executionContext)

        {

            // Create the tracing service

            ITracingService tracingService = executionContext.GetExtension<ITracingService>();

 

            if (tracingService == null)

            {

                throw new InvalidPluginExecutionException("Failed to retrieve tracing service.");

            }

 

            tracingService.Trace("Entered ShareSecuredField.Execute(), Activity Instance Id: {0}, Workflow Instance Id: {1}", executionContext.ActivityInstanceId, executionContext.WorkflowInstanceId);

 

            // Create the context

            IWorkflowContext context = executionContext.GetExtension<IWorkflowContext>();

 

            if (context == null)

            {

                throw new InvalidPluginExecutionException("Failed to retrieve workflow context.");

            }

 

            tracingService.Trace("ShareSecuredField.Execute(), Correlation Id: {0}, Initiating User: {1}", context.CorrelationId, context.InitiatingUserId);

 

            IOrganizationServiceFactory serviceFactory = executionContext.GetExtension<IOrganizationServiceFactory>();

 

            IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);

 

            try

            {

                // TODO: Implement your custom Workflow business logic.

                ExecuteCore(executionContext, context, service);

            }

            catch (Exception e)

            {

                tracingService.Trace("Exception: {0}", e.ToString());

 

                // Handle the exception.

                throw;

            }

 

            tracingService.Trace("Exiting ShareSecuredField.Execute(), Correlation Id: {0}", context.CorrelationId);

        }

 

        private void ExecuteCore(CodeActivityContext executionContext, IWorkflowContext context, IOrganizationService service)

        {

            string entityName = context.PrimaryEntityName;

            Guid recordId = context.PrimaryEntityId;

            string[] multiArray = this.AttributeNames.Get(executionContext).ToString().Split(',');

            foreach (string var in multiArray)

            {

                string attributeName = var.Trim();

                EntityReference userToShare = this.UserToShare.Get(executionContext);

                EntityReference teamToShare = this.TeamToShare.Get(executionContext);

                bool allowRead = this.AllowRead.Get(executionContext);

                bool allowUpdate = this.AllowUpdate.Get(executionContext);

 

                if (userToShare != null)

                {

                    Guid userId = userToShare.Id;

                    ShareSecuredFieldCore(service, entityName, attributeName, recordId, userId, allowRead, allowUpdate, false);

                }

                if (teamToShare != null)

                {

                    Guid teamID = teamToShare.Id;

                    ShareSecuredFieldCore(service, entityName, attributeName, recordId, teamID, allowRead, allowUpdate);

                }

            }

        }

        private void ShareSecuredFieldCore(IOrganizationService service, string entityName, string attributeName, Guid recordId, Guid principalId, bool allowRead, bool allowUpdate, bool shareWithTeam = true)

        {

            // Create the request

            RetrieveAttributeRequest attributeRequest = new RetrieveAttributeRequest

            {

                EntityLogicalName = entityName,

                LogicalName = attributeName,

                RetrieveAsIfPublished = true

            };

 

            // Execute the request

            RetrieveAttributeResponse attributeResponse = (RetrieveAttributeResponse)service.Execute(attributeRequest);

 

            if (attributeResponse.AttributeMetadata != null && attributeResponse.AttributeMetadata.IsSecured != null && attributeResponse.AttributeMetadata.IsSecured.HasValue && attributeResponse.AttributeMetadata.IsSecured.Value)

            {

                // Create the query for retrieve User Shared Attribute permissions.

                QueryExpression queryPOAA = new QueryExpression("principalobjectattributeaccess");

                queryPOAA.ColumnSet = new ColumnSet(new string[] { "readaccess", "updateaccess" });

                queryPOAA.Criteria.FilterOperator = LogicalOperator.And;

                queryPOAA.Criteria.Conditions.Add(new ConditionExpression("attributeid", ConditionOperator.Equal, attributeResponse.AttributeMetadata.MetadataId));

                queryPOAA.Criteria.Conditions.Add(new ConditionExpression("objectid", ConditionOperator.Equal, recordId));

                queryPOAA.Criteria.Conditions.Add(new ConditionExpression("principalid", ConditionOperator.Equal, principalId));

 

                // Execute the query.

                EntityCollection responsePOAA = service.RetrieveMultiple(queryPOAA);

                //throw new InvalidPluginExecutionException("del" + attributeResponse.AttributeMetadata.MetadataId.ToString()+" "+

                //    recordId.ToString()+" " +principalId.ToString());

                if (responsePOAA.Entities.Count > 0)

                {

                    Entity poaa = responsePOAA.Entities[0];

 

                    if (allowRead || allowUpdate)

                    {

                        poaa["readaccess"] = allowRead;

                        poaa["updateaccess"] = allowUpdate;

 

                        service.Update(poaa);

                        // throw new InvalidPluginExecutionException("up");

                    }

                    else

                    {

                        service.Delete("principalobjectattributeaccess", poaa.Id);

                        // throw new InvalidPluginExecutionException("del"+ responsePOAA.Entities.Count.ToString());

                    }

                }

                else

                {

                    if (allowRead || allowUpdate)

                    {

                        // Create POAA entity for user

                        Entity poaa = new Entity("principalobjectattributeaccess");

                        poaa["attributeid"] = attributeResponse.AttributeMetadata.MetadataId;

                        poaa["objectid"] = new EntityReference(entityName, recordId);

                        poaa["readaccess"] = allowRead;

                        poaa["updateaccess"] = allowUpdate;

                        if (shareWithTeam)

                        {

                            poaa["principalid"] = new EntityReference("team", principalId);

                        }

                        else

                        {

                            poaa["principalid"] = new EntityReference("systemuser", principalId);

                        }

 

                        service.Create(poaa);

 

                    }

                }

            }

        }

    }

}


Create workflow on create and Assign of record.


 

Make sure workflow set Execute As “The owner of the workflow”. And owner of workflow have permission to change secure field permission or system admin.

Call custom workflow from newly created workflow.

Now Configure parameters of custom workflow.


For “Attributes Name” use logical name of secure fields.

Example: - emailaddress,phone,mobile.

 

This is completed our second requirement as half of third requirement.

We need to change secure field permission on record is shared or sharing is changed.

For that Dynamics CRM provides three messages that we can use to complete our requirement.

·         GrantAccess

·         RevokeAccess

·         ModifyAccess

Below plugin need to register on these messages to create/update/delete sharing permission.

 

using Microsoft.Xrm.Sdk;

using Microsoft.Xrm.Sdk.Client;

using Microsoft.Xrm.Sdk.Query;

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Threading.Tasks;

using Microsoft.Crm.Sdk.Messages;

using Microsoft.Xrm.Sdk.Messages;

 

namespace CustomWorkflow.Custom_Plugin

{

    public class OnShareRecordShareFLS : IPlugin

    {

        public void Execute(IServiceProvider serviceProvider)

        {

            // Obtain the execution context from the service provider.

            IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));

            IOrganizationServiceFactory serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));

            IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);

            Post_Message_GrantOrRevokeAccess(service, context);

        }

        //this one for Unshare and Share Registration

        //pass the context here from your common Execute function

        void Post_Message_GrantOrRevokeAccess(IOrganizationService service, IPluginExecutionContext context)

        {

            EntityReference EntityRef = (EntityReference)context.InputParameters["Target"];

            string entityName = EntityRef.LogicalName;

            Guid entityId = context.PrimaryEntityId;

            if (EntityRef.LogicalName == "contact")

            {

                if (context.MessageName == "GrantAccess")

                {

                    PrincipalAccess PrincipalAccess = (PrincipalAccess)context.InputParameters["PrincipalAccess"];

                    bool allowRead = true;

                    bool allowUpdate = false;

 

                    if (PrincipalAccess.AccessMask.ToString().Contains("WriteAccess"))

                        allowUpdate = true;

                    //throw new InvalidPluginExecutionException(PrincipalAccess.AccessMask.ToString());

                    EntityReference userOrTeam = PrincipalAccess.Principal;

                    Guid objectId = userOrTeam.Id;

 

                    OrganizationRequest req = new OrganizationRequest("asia_FLSAddRemove");

                    req["Target"] = EntityRef;

                    if (userOrTeam.LogicalName == "systemuser")

                        req["UserToShare"] = userOrTeam;

                    if (userOrTeam.LogicalName == "team")

                        req["TeamToShare"] = userOrTeam;

                    req["AllowRead"] = allowRead;

                    req["AllowUpdate"] = allowUpdate;

                    OrganizationResponse response = service.Execute(req);

 

 

                }

                else if (context.MessageName == "RevokeAccess")

                {

                    EntityReference Revokee = (EntityReference)context.InputParameters["Revokee"];

                    bool allowRead = false;

                    bool allowUpdate = false;

                    OrganizationRequest req = new OrganizationRequest("asia_FLSAddRemove");

 

                    req["Target"] = EntityRef;

                    if (Revokee.LogicalName == "systemuser")

                        req["UserToShare"] = Revokee;

                    if (Revokee.LogicalName == "team")

                        req["TeamToShare"] = Revokee;

                    req["AllowRead"] = allowRead;

                    req["AllowUpdate"] = allowUpdate;

                    OrganizationResponse response = service.Execute(req);

                    // throw new InvalidPluginExecutionException(Revokee.LogicalName);

                }

                else if (context.MessageName == "ModifyAccess")

                {

                    PrincipalAccess PrincipalAccess = (PrincipalAccess)context.InputParameters["PrincipalAccess"];

                    bool allowRead = true;

                    bool allowUpdate = false;

                    if (PrincipalAccess.AccessMask.ToString().Contains("WriteAccess"))

                        allowUpdate = true;

                    //throw new InvalidPluginExecutionException(PrincipalAccess.AccessMask.ToString());

                    EntityReference userOrTeam = PrincipalAccess.Principal;

                    Guid objectId = userOrTeam.Id;

 

                    OrganizationRequest req = new OrganizationRequest("asia_FLSAddRemove");

                    req["Target"] = EntityRef;

                    if (userOrTeam.LogicalName == "systemuser")

                        req["UserToShare"] = userOrTeam;

                    if (userOrTeam.LogicalName == "team")

                        req["TeamToShare"] = userOrTeam;

                    req["AllowRead"] = allowRead;

                    req["AllowUpdate"] = allowUpdate;

                    OrganizationResponse response = service.Execute(req);

 

 

                    //Then got the User or Team and also Access Control that being Granted

 

                    //***to Get User/Team that being Shared With

 

 

 

                }

                else if (context.MessageName == "Assign")

                {

 

                    if (context.PreEntityImages.Contains("preImage") && context.PreEntityImages["preImage"] is Entity)

                    {

                        Entity preMessageImage = (Entity)context.PreEntityImages["preImage"];

                        // get topic field value before database update perform

                        EntityReference previousOwner = (EntityReference)preMessageImage.Attributes["ownerid"];

                        bool allowRead = false;

                        bool allowUpdate = false;

                        OrganizationRequest reqInner = new OrganizationRequest("asia_FLSAddRemove");

                        reqInner["Target"] = EntityRef;

                        if (previousOwner.LogicalName == "systemuser")

                            reqInner["UserToShare"] = new EntityReference("systemuser", previousOwner.Id);

                        reqInner["AllowRead"] = allowRead;

                        reqInner["AllowUpdate"] = allowUpdate;

                        OrganizationResponse responseinner = service.Execute(reqInner);

                    }

                }

            }

        }

    }

}

 

In this plugin we have called a “Action”. So in future if we need to add or remove any attributes than we can do from UI using “Action” and no need to change plugin code.

 

Action: -

This action has four Input parameters to use.


Call our custom workflow from Action.

Configure Custom workflow parameters.

Now time to register the plugins.


While registering plugin make sure you use any admin user or System (Disabled) user.

If want to remove access of secure fields from previous owner add register plugin on “Assign” message and use “preImage”.


Ribbon: -

It’s better to hide or disable “Share secure fields” button for normal expect admin so it will not confuse users.



Field security Profile: - 

This is our last step create a new field security profile and add users who need create permission of secure fields. With help of this user able to create record with secure field.

Now we share record it also shares secure attributes and sync with Read and Write of sharing record.


Once record is saved need to refresh form once to read secure field.

Thanks for reading. HAPPY CRM !!       Follow me to for new updates. 


Field Security Profile - Based on Owner

 Recently received requirement related to Field security profile. Expectation : - 1.       Set to users need access of secure attributes. 2....

Test