web
You’re offline. This is a read only version of the page.
close
Skip to main content

Notifications

Announcements

No record found.

Community site session details

Community site session details

Session Id :

How to get security role privilege changes in Dynamics 365 via c# plugin

Arsen Aghajanyan Profile Picture Arsen Aghajanyan 221

Introduction

As we know Dynamics 365 Customer Engagement has a strong three-layer security model including role-based security, record-based security and field-based security. Today, we're interested in role-based security, particularly how to track privilege changes of a specific role. We'll be using ReplacePrivileges Plugin Message which is available within the Plugin Registration Tool. This message triggers whenever you add, change or remove any privilege from the security role.

Capture1.PNG

You can fire any custom logic on this message. For example, if you need to restrict or warn users from changing a particular privilege. But for now, let's take a look at how we can track the changes.

 

Sample Code

Here is a sample code I wrote to achieve this:

using Microsoft.Crm.Sdk.Messages;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace GetPrivilegeChanges
{
    public class GetPrivilegeChanges : IPlugin
    {
        public void Execute(IServiceProvider serviceProvider)
        {
            IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
            IOrganizationServiceFactory serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
            IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);
            ITracingService tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));

            if (context.InputParameters.Contains("RoleId"))
            {
                Guid roleId = (Guid)context.InputParameters["RoleId"];

                if (context.MessageName != "ReplacePrivileges")
                    return;

                try
                {
                    // Get role privileges before and after the update
                    List<Privilege> oldPrivileges = GetOldPrivileges(roleId, service);
                    List<Privilege> newPrivileges = GetNewPrivileges(service, context);
                                       
                    // Sort privilege lists by the name
                    oldPrivileges = oldPrivileges.OrderBy(o => o.Name).OrderBy(o => o.AccessRight).ToList();
                    newPrivileges = newPrivileges.OrderBy(o => o.Name).OrderBy(o => o.AccessRight).ToList();

                    // Get the privilege differences
                    string differences = GetPrivilegeDifferences(oldPrivileges, newPrivileges);
                    tracingService.Trace(differences);
                }
                catch (Exception ex)
                {
                    throw new InvalidPluginExecutionException("An error occurred in the plug-in. " + ex);
                }
            }
        }

        public class Privilege
        {
            public string Name { get; set; }
            public int AccessRight { get; set; }
            public int PrivilegeDepthMask { get; set; }

            public Privilege(string name, int accessRight, int privilegeDepthMask)
            {
                Name = name;
                AccessRight = accessRight;
                PrivilegeDepthMask = privilegeDepthMask;
            }
        }

        private List<Privilege> GetOldPrivileges(Guid roleId, IOrganizationService service)
        {
            List<Privilege> oldPrivileges = new List<Privilege>();

            var roleRequest = new RetrieveRolePrivilegesRoleRequest
            {
                RoleId = roleId
            };

            var roleResponse = (RetrieveRolePrivilegesRoleResponse)service.Execute(roleRequest);

            Guid[] privilegeIds = GetPrivilegeIds(roleResponse);

            QueryExpression privilegeQuery = new QueryExpression
            {
                EntityName = "privilege",
                ColumnSet = new ColumnSet("name", "accessright")
            };
            LinkEntity rolePrivileges = new LinkEntity("privilege", "roleprivileges", "privilegeid", "privilegeid", JoinOperator.Inner)
            {
                Columns = new ColumnSet("privilegedepthmask"),
                EntityAlias = "rolePrivileges"
            };
            rolePrivileges.LinkCriteria.AddCondition(new ConditionExpression("privilegeid", ConditionOperator.In, privilegeIds));
            rolePrivileges.LinkCriteria.AddCondition(new ConditionExpression("roleid", ConditionOperator.Equal, roleId));
            privilegeQuery.LinkEntities.Add(rolePrivileges);

            EntityCollection oldPrivilegeCollection = service.RetrieveMultiple(privilegeQuery);

            foreach (Entity oldPrivilege in oldPrivilegeCollection.Entities)
            {
                oldPrivileges.Add(new Privilege(oldPrivilege.GetAttributeValue<string>("name"),
                    oldPrivilege.GetAttributeValue<int>("accessright"),
                    (int)oldPrivilege.GetAttributeValue<AliasedValue>("rolePrivileges.privilegedepthmask").Value));
            }

            return oldPrivileges;
        }

        private List<Privilege> GetNewPrivileges(IOrganizationService service, IPluginExecutionContext context)
        {
            List<Privilege> newPrivileges = new List<Privilege>();

            if (context.InputParameters.Contains("Privileges"))
            {
                RolePrivilege[] privileges = (RolePrivilege[])context.InputParameters["Privileges"];

                foreach (RolePrivilege rolePrivilege in privileges)
                {
                    Entity privilege = GetPrivilege(service, rolePrivilege.PrivilegeId);
                    newPrivileges.Add(new Privilege(privilege.GetAttributeValue<string>("name"), privilege.GetAttributeValue<int>("accessright"),
                        GetDepthEnumNumber(rolePrivilege.Depth)));
                }
            }

            return newPrivileges;
        }

        private Guid[] GetPrivilegeIds(RetrieveRolePrivilegesRoleResponse roleResponse)
        {
            Guid[] privilegeIds = new Guid[roleResponse.RolePrivileges.Length + 1];

            int index = 0;
            foreach (var p in roleResponse.RolePrivileges)
            {
                privilegeIds[index] = p.PrivilegeId;
                index++;
            }

            return privilegeIds;
        }

        private int GetDepthEnumNumber(PrivilegeDepth depth)
        {
            switch ((int)depth)
            {
                case 0:
                    return 1;

                case 1:
                    return 2;

                case 2:
                    return 4;

                case 3:
                    return 8;

                default:
                    return 0;
            }
        }

        private string GetPrivilegeDifferences(List<Privilege> oldPrivileges, List<Privilege> newPrivileges)
        {
            string differences = "";
            int i = 0, j = 0;

            while (i < newPrivileges.Count || j < oldPrivileges.Count)
            {
                // If this is something related to Sharepoint permissions continue. These permissions are not visible in the
                // security role UI and are part of the role XML definition.
                if (j < oldPrivileges.Count && (oldPrivileges[j].Name == "prvCreateSharePointData" ||
                    oldPrivileges[j].Name == "prvReadSharePointData" || oldPrivileges[j].Name == "prvWriteSharePointData" ||
                    oldPrivileges[j].Name == "prvReadSharePointDocument"))
                {
                    j++;
                    continue;
                }

                // if we run out of new privileges or old privilege name is alphabetically smaller than new privilege name
                // then the old privilege is removed and is no longer a part of the role.
                if (j < oldPrivileges.Count && (i >= newPrivileges.Count || string.Compare(newPrivileges[i].Name, oldPrivileges[j].Name, ignoreCase: true) > 0))
                {
                    differences += $"Privilege {oldPrivileges[j].Name} {GetAccessRight(oldPrivileges[j].AccessRight)} with privilege depth {GetPrivilegeDepthMask(oldPrivileges[j].PrivilegeDepthMask)} was removed.\n";
                    j++;
                    continue;
                }

                // if we run out of old privileges or new privilege name is alphabetically smaller than old privilege name
                // then the new privilege is added and is a part of the role.
                if (i < newPrivileges.Count && (j >= oldPrivileges.Count || string.Compare(newPrivileges[i].Name, oldPrivileges[j].Name, ignoreCase: true) < 0))
                {
                    differences += $"Privilege {newPrivileges[i].Name} {GetAccessRight(newPrivileges[i].AccessRight)} with privilege depth {GetPrivilegeDepthMask(newPrivileges[i].PrivilegeDepthMask)} was added.\n";
                    i++;
                    continue;
                }

                // If privileges are the same check whether acces right is changed or no.
                if (j < oldPrivileges.Count && i < newPrivileges.Count && 
                    newPrivileges[i].Name == oldPrivileges[j].Name && newPrivileges[i].AccessRight == oldPrivileges[j].AccessRight)
                {
                    if (newPrivileges[i].PrivilegeDepthMask != oldPrivileges[j].PrivilegeDepthMask)
                    {
                        differences += $"Privilege {newPrivileges[i].Name} {GetAccessRight(newPrivileges[i].AccessRight)} was changed from {GetPrivilegeDepthMask(oldPrivileges[j].PrivilegeDepthMask)} to {GetPrivilegeDepthMask(newPrivileges[i].PrivilegeDepthMask)}.\n";
                    }

                    i++;
                    j++;
                    continue;
                }
            }

            return differences;
        }

        private string GetPrivilegeDepthMask(int privilegeDepthMask)
        {
            switch (privilegeDepthMask)
            {
                case 1:
                    return "Basic(User)";

                case 2:
                    return "Local(Business Unit)";

                case 4:
                    return "Deep(Parent: Child)";

                case 8:
                    return "Global(Organisation)";

                default:
                    return "";
            }
        }

        private string GetAccessRight(int accessRight)
        {
            switch (accessRight)
            {
                case 1:
                    return "READ";

                case 2:
                    return "WRITE";

                case 4:
                    return "APPEND";

                case 16:
                    return "APPENDTO";

                case 32:
                    return "CREATE";

                case 65536:
                    return "DELETE";

                case 262144:
                    return "SHARE";

                case 524288:
                    return "ASSIGN";

                default:
                    return "";
            }
        }

        private Entity GetPrivilege(IOrganizationService service, Guid guidPrivilegeId)
        {
            return service.Retrieve("privilege", guidPrivilegeId, new ColumnSet("name", "accessright"));
        }
    }
}

Okay, this looks quite a few lines of code! But it's absolutely straight-forward. Let's examine it.

The context of ReplacePrivileges message has RoleId and Privileges input parameters. First one contains the GUID of the role for which a change was triggered while the second one contains an array of RolePrivilege objects which shows all the role privileges after the update. In order to track the differences, we need to get the privileges before the update as well. (This is the main reason to register a step as a pre-operation! See Registering The Plugin section) The first two lines of the Execute method are just retrieving old and new privileges respectively. For simplicity, we have a simple class Privilege which has only three properties and a simple constructor to set those values. So we keep the old and new privileges in the lists of Privilege class. In the GetOldPrivileges method, we're using RetrieveRolePrivilegesRoleRequest request which retrieves all the privileges associated with the provided role. Having all the privilege ids we need, we're querying the actual privilege records to get necessary information regarding access rights and privilege depths. Looks simple, right? The GetNewPrivileges is even simpler. We're just getting the input parameter provided by the message, querying the record for additional information. 

Next, we have several methods which consist of a single switch case statement. These are just mapping methods. Note that access right and privilege depth are just numbers so we need to map them to some meaningful labels. This is what GetAccessRight and GetPrivilegeDepthMask methods are for. Note that we also have GetDepthEnumNumber method which takes an integer as an input and returns another integer! This looks weird at first. The problem is that Depth property of RolePrivilege class and privilegedepthmask field of the roleprivileges record store the value with different logic. So, we need to map those values to get a similar result. 

After we get old and new privileges for the provided role, we're sorting those lists with a simple LINQ statement. We do this to make the comparison between two lists a bit easier. I'm quite sure there's a lot of other ways to do this via LINQ or another way, which will be a lot easier to understand but in this case, I wrote a custom GetPrivilegeDifferences method, which will evaluate all the differences. This method will work in O(n + m) time, where n is the length of the new privileges list and m is the length of the old privileges list. I find this quite a performant solution.

It's not difficult to understand the logic behind GetPrivilegeDifferences method. There we have 4 cases. First, there are several SharePoint related privileges which are not editable via Dynamics 365 security role UI. These are part of the security role XML. Problem is we're not getting those roles from the message input. This means we won't have them in the new privileges list. In order to bypass this, we just keep on going if we find a privilege with a hardcoded name(I found 4 of them, is there any more?). Next, we have two statements in case if current new privilege and old privilege have different names. That means that a privilege was either added or removed. And lastly, we have a statement in case if the depth of a privilege was changed. 

Registering the Plugin Step

In order to register the step, we're deploying the plugin .dll file as usual. It's called GetPrivilegeChanges in my case. After that, we need to register the actual step. We should register RepliacePrivilege message which is available only for the role entity(obviously!) as a pre-operation step!

Capture2.PNG

Note! Registering this step in the pre-operation stage is very important. You won't be able to query old privileges if the stage is post-operation. Also, you're not able to use entity images with this message.

Result


Let's take a look at what we've got with an example. Say, we have this Scheduler role with some privileges.

Capture3.PNG

Let's change some of the privileges. For example, let's add an Account Read privilege with Business Unit depth. Change the Activity Write privilege to the Global depth and remove the Connection Role Create privilege at all!

Capture4.PNG


This change will fire up the plugin. Let's investigate the result in the plugin trace log record.

Capture.PNG

Okay, so we've got what we've expected to get! Now, we can use this information, for example, to send an email to keep the system administrators updated about what has been changed or use it as required for our business needs.

Also, notice the execution duration which was almost 9 seconds in this case. I find this quite a good time considering that we had almost 700 privileges for this role! 

I hope you'll find something useful out of this post.

Happy Codding! :) 

Comments

*This post is locked for comments