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?
| Property | Value |
|---|---|
| Entity | Contact to Assignment Relationship (tt_Contact_tt_Assignment) |
| Message | Disassociate |
| Stage | Post-Operation (runs after the disassociation is deleted) |
| Execution Mode | Synchronous |
| Conditions | Only 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
Targetparameter (Contact EntityReference) - Extracts
RelatedEntitiesparameter (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_webroleentity for matchingadx_name - Skips this Web Role if not found
Sub-step 9c: Disassociate Contact from Web Role
- Removes the many-to-many relationship using
Disassociatemethod - Unlinks Contact from Web Role via
adx_webrole_contactrelationship - 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:
- Verify the Contact doesn't have other Assignments requiring the same Web Roles
- Check the Assignment name exactly matches a key in the "Assignments.json" configuration
- Query the
tt_configurationentity for a record named "Assignments.json" - Review the Plugin Trace Log for "still required by another assignment" messages
- Confirm the plugin is registered on Disassociate message for the
tt_Contact_tt_Assignmentrelationship
Problem: Plugin throws "Assignments.json configuration not found"
Cause: The Configuration entity doesn't have a record named "Assignments.json"
Solution:
- Create a Configuration record with:
- Name = "Assignments.json"
- Content (tt_content) = Valid JSON mapping assignments to web roles
- 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:
- Verify the JSON configuration structure is correct
- Check that shared Web Roles are listed under all appropriate Assignments
- Review the Plugin Trace Log to see which Assignments are found as "remaining"
- 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:
- Review what other Assignments the Contact has
- Check the configuration to see which Web Roles those Assignments grant
- Review the Plugin Trace Log for "still required by another assignment" messages
- 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:
- Review the
adx_webroleentity and verify exact names (including spaces and capitalization) - Check the Plugin Trace Log for the exact Web Role name being searched
- Ensure Web Role names in the configuration exactly match those in Dynamics
- 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:
- Review the Contact's Assignment history to see what was removed and when
- Check the configuration to ensure shared Web Roles are listed under all appropriate Assignments
- Review the Plugin Trace Log for multiple disassociation events
- 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
Related Documentation
- AssociateEntitiesPostOperationUpdate - Companion plugin that adds Web Roles when Assignments are created
- Utils.ValidateContactAssignmentRelationship() - Utility method that validates the Disassociate relationship
- Utils.GetAssignmentConfiguration() - Utility method that retrieves the Assignments.json configuration
- Utils.GetWebRolesForAssignment() - Utility method that extracts Web Roles for an Assignment
- Utils.GetWebRoleByName() - Utility method that queries Web Role records
- Assignment Configuration Guide [To Be Completed] - How to configure Assignment to Web Role mappings
- Web Roles Overview [To Be Completed] - Understanding Web Roles and portal permissions
Configuration Requirements
This plugin depends on the following configuration:
-
Configuration Record (
tt_configuration)- Name = "Assignments.json"
- Content (tt_content) = JSON mapping of Assignment names to Web Roles
- State = Active
-
JSON Structure:
{
"Assignment Name": {
"Web Roles": [
"Web Role Name 1",
"Web Role Name 2"
]
}
} -
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
-
Assignments (
tt_assignment)- Must have
tt_namefield populated - Names should match keys in configuration
- Must have
-
Relationship (
tt_Contact_tt_Assignment)- Many-to-many relationship between Contact and Assignment entities
- Must be properly configured in Dynamics
-
Plugin Registration
- Registered on Disassociate message for
tt_Contact_tt_Assignmentrelationship - Post-Operation stage
- Synchronous execution mode
- Registered on Disassociate message for
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:
- Identifies Web Roles from removed Assignment: ["Business Tax - Data Provider", "Dashboard - Corporates"]
- Checks remaining Assignment "Audit - Data Provider" grants: ["Audit - Data Provider", "Dashboard - Corporates"]
- Removes: "Business Tax - Data Provider" (not needed by remaining Assignment)
- 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