Skip to main content

Contact Assignment Web Roles - Post Operation Disassociate

This plugin automatically removes a Contact's Web Roles when they are disassociated from an Assignment, but only if those Web Roles are not required by other Assignments.


What Does This Do?

When a Contact is removed from an Assignment (like "Audit Team Member" or "Tax Advisory Client"), this plugin intelligently removes web portal permissions that are no longer needed—but only if they're not still required by another Assignment the Contact has.

For example, if a contact is removed from "Business Tax - Data Provider" but still has "Audit - Data Provider", this plugin will:

  • Identify which Web Roles were granted by "Business Tax - Data Provider"
  • Check if any of the Contact's remaining Assignments require those same Web Roles
  • Remove only the Web Roles that are no longer needed
  • Keep Web Roles that are still required by other Assignments

Important: This only runs when using the "Disassociate" action to unlink a Contact from an Assignment through the tt_Contact_tt_Assignment relationship.


When Does This Run?

PropertyValue
EntityContact to Assignment Relationship (tt_Contact_tt_Assignment)
MessageDisassociate
StagePost-Operation (runs after the disassociation is deleted)
Execution ModeSynchronous
ConditionsOnly runs when Contact is disassociated from Assignment

How It Works

Step 1: Validate the Disassociate message

The plugin first confirms it was triggered by a "Disassociate" action. If triggered by any other message (Create, Update, etc.), it throws an exception.

Exit condition: Throws InvalidPluginExecutionException if message is not "Disassociate"

Step 2: Validate the relationship

Checks that the disassociation being deleted is specifically between a Contact and an Assignment using the tt_Contact_tt_Assignment relationship. If it's a different relationship, the plugin exits silently.

What happens: Validates relationship schema name, Target entity (Contact), and RelatedEntities (Assignment)

Exit condition: Returns early if relationship is not tt_Contact_tt_Assignment or entity types don't match

Step 3: Extract Contact and Assignment references

Retrieves the Contact ID and Assignment ID from the plugin execution context's input parameters.

What happens:

  • Extracts Target parameter (Contact EntityReference)
  • Extracts RelatedEntities parameter (Assignment EntityReference)
  • Logs the IDs for tracing
Step 4: Get the removed Assignment name

Retrieves the full Assignment record to get its name (e.g., "Business Tax - Data Provider"). This name is used to look up which Web Roles should potentially be removed.

What happens: Queries the Assignment entity for the tt_name field

Step 5: Load the Assignment-to-Web-Role configuration

Retrieves the configuration from the tt_configuration entity. This configuration is stored as JSON and maps Assignment names to lists of Web Roles.

What happens:

  • Queries for configuration named "Assignments.json"
  • Parses JSON content into a nested dictionary structure
  • Returns null if configuration is not found

Exit condition: Returns early if configuration is missing or removed Assignment is not in configuration

Step 6: Find Web Roles that were granted by removed Assignment

Searches the configuration dictionary for the removed Assignment name and extracts the list of Web Roles that were originally granted.

What happens:

  • Looks up removed Assignment name in configuration
  • Retrieves the "Web Roles" array for that assignment
  • Returns empty list if no Web Roles are configured

Exit condition: Returns early if no Web Roles are found for the removed Assignment

Step 7: Check Contact's remaining Assignments

Queries the tt_contact_tt_assignment intersection table to find all other Assignments the Contact still has.

What happens:

  • Retrieves all Assignment IDs still linked to this Contact
  • Logs the count of remaining assignments
Step 8: Build a "keep list" of Web Roles

For each remaining Assignment, retrieves its Web Roles from the configuration and adds them to a HashSet. This creates a list of all Web Roles the Contact should retain.

What happens:

  • Iterates through each remaining Assignment
  • Retrieves the Assignment name
  • Looks up Web Roles in configuration
  • Adds all Web Roles to a HashSet (automatically handles duplicates)
Step 9: Remove Web Roles that are no longer needed

Iterates through the Web Roles that were granted by the removed Assignment and disassociates the Contact from those that are not in the "keep list".

For each Web Role from removed Assignment:

Sub-step 9a: Check if Web Role should be kept

  • If the role is in the "keep list", skip it and log why

Sub-step 9b: Look up the Web Role record by name

  • Queries adx_webrole entity for matching adx_name
  • Skips this Web Role if not found

Sub-step 9c: Disassociate Contact from Web Role

  • Removes the many-to-many relationship using Disassociate method
  • Unlinks Contact from Web Role via adx_webrole_contact relationship
  • Logs success
Step 10: Log completion

Records that the plugin has finished processing in the Plugin Trace Log. This helps with debugging and verifying the plugin executed correctly.

What happens: Writes a trace message indicating successful completion


Why This Matters

This plugin ensures that Web Role permissions are automatically cleaned up when someone is removed from an Assignment, but it's smart enough to avoid removing permissions that are still needed. This is critical because:

  • Prevents permission bloat - Removes access that's no longer needed
  • Maintains necessary access - Keeps permissions if another Assignment requires them
  • Reduces security risk - Ensures users only have the permissions they should have
  • Automates cleanup - No manual permission removal required
  • Prevents accidental lockouts - Won't remove a Web Role if it's needed by another Assignment
  • Maintains audit trail - All removals are logged in the trace log

Code

using System;
using System.Collections.Generic;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;

namespace PracticeGateway.Plugin
{
public class DisassociateEntitiesPostOperationUpdate : PluginBase
{
public DisassociateEntitiesPostOperationUpdate(string unsecure, string secure)
: base(typeof(DisassociateEntitiesPostOperationUpdate))
{
}

/// <summary>
/// This plugin runs after a Contact is removed from an Assignment.
/// It removes Web Roles that are no longer needed (unless another Assignment still requires them).
/// </summary>
protected override void ExecuteCdsPlugin(ILocalPluginContext localContext)
{
if (localContext == null)
throw new InvalidPluginExecutionException(nameof(localContext));

// Get the services we need to work with data and log messages
ITracingService tracingService = localContext.TracingService;
IPluginExecutionContext context = localContext.PluginExecutionContext;
IOrganizationService currentUserService = localContext.CurrentUserService;

try
{
tracingService.Trace("=== Starting DisassociateEntitiesPostOperationUpdate ===");

// Make sure this plugin was triggered by a "Disassociate" action
if (context.MessageName != "Disassociate")
throw new InvalidPluginExecutionException($"Unexpected message: {context.MessageName}");

// Helper class for common operations
Utils.Utils utils = new Utils.Utils(currentUserService, tracingService);

// Get the Contact and Assignment that were just unlinked
if (!utils.ValidateContactAssignmentRelationship(context, tracingService, out EntityReference contactRef, out EntityReference assignmentRef))
{
return; // Stop if the relationship isn't what we expect
}

Guid contactId = contactRef.Id;
Guid removedAssignmentId = assignmentRef.Id;

tracingService.Trace($"Processing disassociation of contact {contactId} from assignment {removedAssignmentId}");

// Get the Assignment's name (we need this to find which web roles to remove)
Entity removedAssignment = currentUserService.Retrieve("tt_assignment", removedAssignmentId, new ColumnSet("tt_name"));
string removedAssignmentName = removedAssignment.GetAttributeValue<string>("tt_name");

tracingService.Trace($"Removed assignment name: {removedAssignmentName}");

// Load the configuration that maps Assignments to Web Roles
var config = utils.GetAssignmentConfiguration(currentUserService, tracingService);
if (config == null || !config.ContainsKey(removedAssignmentName))
{
tracingService.Trace($"No configuration found for assignment '{removedAssignmentName}'");
return;
}

// Find which Web Roles were linked to the removed Assignment
List<string> removedRoles = utils.GetWebRolesForAssignment(config, removedAssignmentName, tracingService);
if (removedRoles.Count == 0)
{
tracingService.Trace($"No web roles configured for removed assignment '{removedAssignmentName}'");
return;
}

// Check what other Assignments this Contact still has
QueryExpression remainingAssignmentsQuery = new QueryExpression("tt_contact_tt_assignment")
{
ColumnSet = new ColumnSet("tt_assignmentid"),
Criteria = new FilterExpression
{
Conditions = { new ConditionExpression("contactid", ConditionOperator.Equal, contactId) }
}
};

EntityCollection remainingAssignments = currentUserService.RetrieveMultiple(remainingAssignmentsQuery);
tracingService.Trace($"Found {remainingAssignments.Entities.Count} remaining assignments for contact.");

// Build a list of Web Roles the Contact should keep (from their other Assignments)
HashSet<string> rolesToKeep = new HashSet<string>();

foreach (var assignmentEntity in remainingAssignments.Entities)
{
Guid assignmentId = assignmentEntity.GetAttributeValue<Guid>("tt_assignmentid");
Entity assignment = currentUserService.Retrieve("tt_assignment", assignmentId, new ColumnSet("tt_name"));
string assignmentName = assignment.GetAttributeValue<string>("tt_name");

// Add the Web Roles from this Assignment to our "keep" list
List<string> roles = utils.GetWebRolesForAssignment(config, assignmentName, tracingService);
foreach (var role in roles)
{
rolesToKeep.Add(role);
}
}

// Remove Web Roles only if they're not needed by another Assignment
foreach (string roleName in removedRoles)
{
// Don't remove the role if another Assignment still needs it
if (rolesToKeep.Contains(roleName))
{
tracingService.Trace($"Web role '{roleName}' is still required by another assignment. Skipping disassociation.");
continue;
}

// Look up the Web Role record by name
Entity webRole = utils.GetWebRoleByName(currentUserService, roleName);
if (webRole == null)
{
tracingService.Trace($"Web role '{roleName}' not found.");
continue;
}

// Unlink the Contact from the Web Role
currentUserService.Disassociate(
"contact",
contactId,
new Relationship("adx_webrole_contact"),
new EntityReferenceCollection { new EntityReference("adx_webrole", webRole.Id) }
);

tracingService.Trace($"Successfully disassociated contact from web role '{roleName}'.");
}

tracingService.Trace("=== Finished DisassociateEntitiesPostOperationUpdate ===");
}
catch (Exception ex)
{
// Log the error and stop the plugin
tracingService.Trace($"Error in DisassociateEntitiesPostOperationUpdate: {ex}");
throw new InvalidPluginExecutionException("Plugin execution failed.", ex);
}
}
}
}

Common Issues & Troubleshooting

Problem: Web Roles aren't being removed when I remove a Contact from an Assignment

Possible Causes:

  • The Contact has another Assignment that requires the same Web Roles
  • The removed Assignment name doesn't match any entry in the configuration
  • The configuration record is missing or inactive
  • The plugin registration might be missing or disabled
  • The relationship being used is not tt_Contact_tt_Assignment

How to Check:

  1. Verify the Contact doesn't have other Assignments requiring the same Web Roles
  2. Check the Assignment name exactly matches a key in the "Assignments.json" configuration
  3. Query the tt_configuration entity for a record named "Assignments.json"
  4. Review the Plugin Trace Log for "still required by another assignment" messages
  5. Confirm the plugin is registered on Disassociate message for the tt_Contact_tt_Assignment relationship

Problem: Plugin throws "Assignments.json configuration not found"

Cause: The Configuration entity doesn't have a record named "Assignments.json"

Solution:

  1. Create a Configuration record with:
    • Name = "Assignments.json"
    • Content (tt_content) = Valid JSON mapping assignments to web roles
  2. Ensure the record is active

Example JSON structure:

{
"Business Tax - Data Provider": {
"Web Roles": [
"Business Tax - Data Provider",
"Dashboard - Corporates"
]
},
"Audit - Primary Contact": {
"Web Roles": [
"Audit - Primary Contact",
"Dashboard - Corporates"
]
}
}

Problem: Too many Web Roles are being removed

Possible Causes:

  • The configuration has Web Roles listed under the wrong Assignment
  • Other Assignments that should share the same Web Roles are not configured correctly
  • The "keep list" logic is not finding remaining Assignments

How to Check:

  1. Verify the JSON configuration structure is correct
  2. Check that shared Web Roles are listed under all appropriate Assignments
  3. Review the Plugin Trace Log to see which Assignments are found as "remaining"
  4. Confirm the Contact's remaining Assignments are properly linked in the database

Problem: No Web Roles are being removed (all are kept)

Possible Causes:

  • The Contact has other Assignments that require all the same Web Roles
  • The removed Assignment's Web Roles are also listed under remaining Assignments

How to Check:

  1. Review what other Assignments the Contact has
  2. Check the configuration to see which Web Roles those Assignments grant
  3. Review the Plugin Trace Log for "still required by another assignment" messages
  4. This may actually be correct behavior if the Web Roles are legitimately needed

Problem: Plugin throws "Web role 'X' not found"

Possible Causes:

  • The Web Role name in the configuration doesn't match the actual Web Role name in Dynamics
  • The Web Role has been deleted or deactivated
  • Web Role name has incorrect capitalization or extra spaces

How to Check:

  1. Review the adx_webrole entity and verify exact names (including spaces and capitalization)
  2. Check the Plugin Trace Log for the exact Web Role name being searched
  3. Ensure Web Role names in the configuration exactly match those in Dynamics
  4. Verify the Web Roles are active (statecode = 0)

Problem: Contact loses all portal access unexpectedly

Possible Causes:

  • The removed Assignment was the only one granting a shared Web Role
  • The configuration doesn't properly list shared Web Roles under all Assignments
  • Another Assignment was removed simultaneously or just before

How to Check:

  1. Review the Contact's Assignment history to see what was removed and when
  2. Check the configuration to ensure shared Web Roles are listed under all appropriate Assignments
  3. Review the Plugin Trace Log for multiple disassociation events
  4. Verify which Web Roles were removed in the trace log

Solution:

  • Update the configuration to include shared Web Roles under all Assignments that need them
  • Re-associate the Contact with an appropriate Assignment to restore access


Configuration Requirements

This plugin depends on the following configuration:

  1. Configuration Record (tt_configuration)

    • Name = "Assignments.json"
    • Content (tt_content) = JSON mapping of Assignment names to Web Roles
    • State = Active
  2. JSON Structure:

    {
    "Assignment Name": {
    "Web Roles": [
    "Web Role Name 1",
    "Web Role Name 2"
    ]
    }
    }
  3. Web Roles (adx_webrole)

    • Web Roles referenced in configuration must exist
    • Names must exactly match those in configuration (case-sensitive)
    • Web Roles must be active
  4. Assignments (tt_assignment)

    • Must have tt_name field populated
    • Names should match keys in configuration
  5. Relationship (tt_Contact_tt_Assignment)

    • Many-to-many relationship between Contact and Assignment entities
    • Must be properly configured in Dynamics
  6. Plugin Registration

    • Registered on Disassociate message for tt_Contact_tt_Assignment relationship
    • Post-Operation stage
    • Synchronous execution mode

Example Scenario

Initial State:

  • Contact has two Assignments:
    • "Business Tax - Data Provider" → grants "Business Tax - Data Provider" and "Dashboard - Corporates"
    • "Audit - Data Provider" → grants "Audit - Data Provider" and "Dashboard - Corporates"
  • Contact has three Web Roles:
    • Business Tax - Data Provider
    • Audit - Data Provider
    • Dashboard - Corporates

Action:

Contact is disassociated from "Business Tax - Data Provider"

Plugin Behavior:

  1. Identifies Web Roles from removed Assignment: ["Business Tax - Data Provider", "Dashboard - Corporates"]
  2. Checks remaining Assignment "Audit - Data Provider" grants: ["Audit - Data Provider", "Dashboard - Corporates"]
  3. Removes: "Business Tax - Data Provider" (not needed by remaining Assignment)
  4. Keeps: "Dashboard - Corporates" (still needed by "Audit - Data Provider")

Final State:

  • Contact has one Assignment:
    • "Audit - Data Provider"
  • Contact has two Web Roles:
    • Audit - Data Provider
    • Dashboard - Corporates (retained!)

Testing Checklist

When testing this plugin, verify:

  • Removing a Contact from an Assignment removes Web Roles that are no longer needed
  • Web Roles shared by multiple Assignments are NOT removed if another Assignment still requires them
  • Removing a Contact's last Assignment removes all associated Web Roles
  • Plugin handles missing configuration gracefully (exits without error)
  • Plugin handles missing Web Roles gracefully (logs warning, continues)
  • Plugin exits silently when triggered on non-Contact/Assignment relationships
  • Plugin throws exception when triggered by non-Disassociate message
  • Removing and re-adding the same Assignment works correctly
  • Multiple Contacts can be disassociated from the same Assignment successfully
  • Plugin completes within acceptable performance timeframe
  • Error messages are logged clearly in Plugin Trace Log
  • Trace log clearly shows which Web Roles were kept vs removed and why
  • Contact doesn't lose portal access if they have overlapping Assignment permissions