This repository was archived by the owner on Dec 15, 2025. It is now read-only.
forked from DynamicsValue/fake-xrm-easy
-
Notifications
You must be signed in to change notification settings - Fork 1
Add core impersonation support with comprehensive documentation #119
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
7713423
Initial plan
Copilot 8cfe18f
Add core impersonation support with CallerProperties.ImpersonatedUserId
Copilot 3a0c2ed
Fix impersonation security logic and entity initialization
Copilot 1f38e39
Fix CallerProperties initialization and GetEffectiveUser logic
Copilot 5bd85f6
Add comprehensive impersonation and MDA documentation
Copilot 36da5b9
Changes before error encountered
Copilot 684975e
Merge branch 'main' into copilot/add-impersonation-support
rnwood File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
277 changes: 277 additions & 0 deletions
277
Fake4DataverseCore/Fake4Dataverse.Core.Tests/Security/ImpersonationTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,277 @@ | ||||||
| using Fake4Dataverse.Security; | ||||||
| using Microsoft.Xrm.Sdk; | ||||||
| using System; | ||||||
| using System.Linq; | ||||||
| using Xunit; | ||||||
|
|
||||||
| namespace Fake4Dataverse.Core.Tests.Security | ||||||
| { | ||||||
| /// <summary> | ||||||
| /// Tests for impersonation functionality. | ||||||
| /// Reference: https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/impersonate-another-user-web-api | ||||||
| /// | ||||||
| /// Impersonation allows a user with the prvActOnBehalfOfAnotherUser privilege to perform operations | ||||||
| /// as if they were another user. The impersonated user's identity is used for security checks and audit fields, | ||||||
| /// while the actual calling user is recorded in createdonbehalfof/modifiedonbehalfof fields. | ||||||
| /// </summary> | ||||||
| public class ImpersonationTests | ||||||
| { | ||||||
| [Fact] | ||||||
| public void Should_Allow_SystemAdministrator_To_Impersonate() | ||||||
| { | ||||||
| // Reference: https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/impersonate-another-user-web-api | ||||||
| // The System Administrator role has the prvActOnBehalfOfAnotherUser privilege by default. | ||||||
|
|
||||||
| // Arrange | ||||||
| var context = new XrmFakedContext(); | ||||||
| context.SecurityConfiguration.SecurityEnabled = true; | ||||||
| var service = context.GetOrganizationService(); | ||||||
|
|
||||||
| var adminUserId = Guid.NewGuid(); | ||||||
| var targetUserId = Guid.NewGuid(); | ||||||
| var businessUnitId = context.SecurityManager.RootBusinessUnitId; | ||||||
|
|
||||||
| // Create admin user and assign System Administrator role | ||||||
| var adminUser = new Entity("systemuser") | ||||||
| { | ||||||
| Id = adminUserId, | ||||||
| ["businessunitid"] = new EntityReference("businessunit", businessUnitId), | ||||||
| ["fullname"] = "Admin User" | ||||||
| }; | ||||||
| context.AddEntity(adminUser); | ||||||
|
|
||||||
| // Create target user | ||||||
| var targetUser = new Entity("systemuser") | ||||||
| { | ||||||
| Id = targetUserId, | ||||||
| ["businessunitid"] = new EntityReference("businessunit", businessUnitId), | ||||||
| ["fullname"] = "Target User" | ||||||
| }; | ||||||
| context.AddEntity(targetUser); | ||||||
|
|
||||||
| // Assign System Administrator role to admin | ||||||
| service.Associate("systemuser", adminUserId, new Relationship("systemuserroles_association"), | ||||||
| new EntityReferenceCollection { new EntityReference("role", context.SecurityManager.SystemAdministratorRoleId) }); | ||||||
|
|
||||||
| // Set caller as admin | ||||||
| context.CallerProperties.CallerId = new EntityReference("systemuser", adminUserId); | ||||||
|
|
||||||
| // Set impersonation | ||||||
| context.CallerProperties.ImpersonatedUserId = new EntityReference("systemuser", targetUserId); | ||||||
|
|
||||||
| // Act - Create an account (should succeed with no exception) | ||||||
| var account = new Entity("account") | ||||||
| { | ||||||
| Id = Guid.NewGuid(), | ||||||
| ["name"] = "Test Account" | ||||||
| }; | ||||||
|
|
||||||
| // Assert - Should not throw | ||||||
| var exception = Record.Exception(() => service.Create(account)); | ||||||
| Assert.Null(exception); | ||||||
| } | ||||||
|
|
||||||
| [Fact] | ||||||
| public void Should_Set_CreatedBy_To_ImpersonatedUser() | ||||||
| { | ||||||
| // Reference: https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/impersonate-another-user-web-api | ||||||
| // When impersonating, createdby should be set to the impersonated user. | ||||||
|
|
||||||
| // Arrange | ||||||
| var context = new XrmFakedContext(); | ||||||
| context.SecurityConfiguration.SecurityEnabled = true; | ||||||
| var service = context.GetOrganizationService(); | ||||||
|
|
||||||
| var adminUserId = Guid.NewGuid(); | ||||||
| var targetUserId = Guid.NewGuid(); | ||||||
| var businessUnitId = context.SecurityManager.RootBusinessUnitId; | ||||||
|
|
||||||
| // Create users | ||||||
| var adminUser = new Entity("systemuser") | ||||||
| { | ||||||
| Id = adminUserId, | ||||||
| ["businessunitid"] = new EntityReference("businessunit", businessUnitId), | ||||||
| ["fullname"] = "Admin User" | ||||||
| }; | ||||||
| context.AddEntity(adminUser); | ||||||
|
|
||||||
| var targetUser = new Entity("systemuser") | ||||||
| { | ||||||
| Id = targetUserId, | ||||||
| ["businessunitid"] = new EntityReference("businessunit", businessUnitId), | ||||||
| ["fullname"] = "Target User" | ||||||
| }; | ||||||
| context.AddEntity(targetUser); | ||||||
|
|
||||||
| // Assign System Administrator role | ||||||
| service.Associate("systemuser", adminUserId, new Relationship("systemuserroles_association"), | ||||||
| new EntityReferenceCollection { new EntityReference("role", context.SecurityManager.SystemAdministratorRoleId) }); | ||||||
|
|
||||||
| // Set impersonation | ||||||
| context.CallerProperties.CallerId = new EntityReference("systemuser", adminUserId); | ||||||
| context.CallerProperties.ImpersonatedUserId = new EntityReference("systemuser", targetUserId); | ||||||
|
|
||||||
| // Act - Create an account | ||||||
| var accountId = Guid.NewGuid(); | ||||||
| var account = new Entity("account") | ||||||
| { | ||||||
| Id = accountId, | ||||||
| ["name"] = "Test Account" | ||||||
| }; | ||||||
| service.Create(account); | ||||||
|
|
||||||
| // Assert - createdby should be target user | ||||||
| var retrieved = service.Retrieve("account", accountId, new Microsoft.Xrm.Sdk.Query.ColumnSet(true)); | ||||||
| Assert.NotNull(retrieved.GetAttributeValue<EntityReference>("createdby")); | ||||||
| Assert.Equal(targetUserId, retrieved.GetAttributeValue<EntityReference>("createdby").Id); | ||||||
| } | ||||||
|
|
||||||
| [Fact] | ||||||
| public void Should_Set_CreatedOnBehalfOf_To_CallingUser() | ||||||
| { | ||||||
| // Reference: https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/impersonate-another-user-web-api | ||||||
| // When impersonating, createdonbehalfof should be set to the actual calling user (the impersonator). | ||||||
|
|
||||||
| // Arrange | ||||||
| var context = new XrmFakedContext(); | ||||||
| context.SecurityConfiguration.SecurityEnabled = true; | ||||||
| var service = context.GetOrganizationService(); | ||||||
|
|
||||||
| var adminUserId = Guid.NewGuid(); | ||||||
| var targetUserId = Guid.NewGuid(); | ||||||
| var businessUnitId = context.SecurityManager.RootBusinessUnitId; | ||||||
|
|
||||||
| // Create users | ||||||
| var adminUser = new Entity("systemuser") | ||||||
| { | ||||||
| Id = adminUserId, | ||||||
| ["businessunitid"] = new EntityReference("businessunit", businessUnitId), | ||||||
| ["fullname"] = "Admin User" | ||||||
| }; | ||||||
| context.AddEntity(adminUser); | ||||||
|
|
||||||
| var targetUser = new Entity("systemuser") | ||||||
| { | ||||||
| Id = targetUserId, | ||||||
| ["businessunitid"] = new EntityReference("businessunit", businessUnitId), | ||||||
| ["fullname"] = "Target User" | ||||||
| }; | ||||||
| context.AddEntity(targetUser); | ||||||
|
|
||||||
| // Assign System Administrator role | ||||||
| service.Associate("systemuser", adminUserId, new Relationship("systemuserroles_association"), | ||||||
| new EntityReferenceCollection { new EntityReference("role", context.SecurityManager.SystemAdministratorRoleId) }); | ||||||
|
|
||||||
| // Set impersonation | ||||||
| context.CallerProperties.CallerId = new EntityReference("systemuser", adminUserId); | ||||||
| context.CallerProperties.ImpersonatedUserId = new EntityReference("systemuser", targetUserId); | ||||||
|
|
||||||
| // Act - Create an account | ||||||
| var accountId = Guid.NewGuid(); | ||||||
| var account = new Entity("account") | ||||||
| { | ||||||
| Id = accountId, | ||||||
| ["name"] = "Test Account" | ||||||
| }; | ||||||
| service.Create(account); | ||||||
|
|
||||||
| // Assert - createdonbehalfof should be admin user (the impersonator) | ||||||
| var retrieved = service.Retrieve("account", accountId, new Microsoft.Xrm.Sdk.Query.ColumnSet(true)); | ||||||
| Assert.True(retrieved.Contains("createdonbehalfof")); | ||||||
| Assert.NotNull(retrieved.GetAttributeValue<EntityReference>("createdonbehalfof")); | ||||||
| Assert.Equal(adminUserId, retrieved.GetAttributeValue<EntityReference>("createdonbehalfof").Id); | ||||||
| } | ||||||
|
|
||||||
| [Fact] | ||||||
| public void Should_Deny_Impersonation_Without_Privilege() | ||||||
| { | ||||||
| // Reference: https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/impersonate-another-user-web-api | ||||||
| // A user without the prvActOnBehalfOfAnotherUser privilege cannot impersonate. | ||||||
|
|
||||||
| // Arrange | ||||||
| var context = new XrmFakedContext(); | ||||||
| context.SecurityConfiguration.SecurityEnabled = true; | ||||||
| var service = context.GetOrganizationService(); | ||||||
|
|
||||||
| var regularUserId = Guid.NewGuid(); | ||||||
| var targetUserId = Guid.NewGuid(); | ||||||
| var businessUnitId = context.SecurityManager.RootBusinessUnitId; | ||||||
|
|
||||||
| // Create regular user (no special privileges) | ||||||
| var regularUser = new Entity("systemuser") | ||||||
| { | ||||||
| Id = regularUserId, | ||||||
| ["businessunitid"] = new EntityReference("businessunit", businessUnitId), | ||||||
| ["fullname"] = "Regular User" | ||||||
| }; | ||||||
| context.AddEntity(regularUser); | ||||||
|
|
||||||
| // Create target user | ||||||
| var targetUser = new Entity("systemuser") | ||||||
| { | ||||||
| Id = targetUserId, | ||||||
| ["businessunitid"] = new EntityReference("businessunit", businessUnitId), | ||||||
| ["fullname"] = "Target User" | ||||||
| }; | ||||||
| context.AddEntity(targetUser); | ||||||
|
|
||||||
| // Set caller as regular user | ||||||
| context.CallerProperties.CallerId = new EntityReference("systemuser", regularUserId); | ||||||
|
|
||||||
| // Set impersonation | ||||||
| context.CallerProperties.ImpersonatedUserId = new EntityReference("systemuser", targetUserId); | ||||||
|
|
||||||
| // Act & Assert - Should throw UnauthorizedAccessException | ||||||
| var account = new Entity("account") | ||||||
| { | ||||||
| Id = Guid.NewGuid(), | ||||||
| ["name"] = "Test Account" | ||||||
| }; | ||||||
|
|
||||||
| var exception = Assert.Throws<UnauthorizedAccessException>(() => service.Create(account)); | ||||||
| Assert.Contains("prvActOnBehalfOfAnotherUser", exception.Message); | ||||||
| } | ||||||
|
|
||||||
| [Fact] | ||||||
| public void Should_Not_Set_CreatedOnBehalfOf_Without_Impersonation() | ||||||
| { | ||||||
| // When not impersonating, createdonbehalfof should not be set. | ||||||
|
|
||||||
| // Arrange | ||||||
| var context = new XrmFakedContext(); | ||||||
| context.SecurityConfiguration.SecurityEnabled = true; | ||||||
| var service = context.GetOrganizationService(); | ||||||
|
|
||||||
| var adminUserId = Guid.NewGuid(); | ||||||
| var businessUnitId = context.SecurityManager.RootBusinessUnitId; | ||||||
|
|
||||||
| // Create admin user | ||||||
| var adminUser = new Entity("systemuser") | ||||||
| { | ||||||
| Id = adminUserId, | ||||||
| ["businessunitid"] = new EntityReference("businessunit", businessUnitId), | ||||||
| ["fullname"] = "Admin User" | ||||||
| }; | ||||||
| context.AddEntity(adminUser); | ||||||
| service.Associate("systemuser", adminUserId, new Relationship("systemuserroles_association"), | ||||||
| new EntityReferenceCollection { new EntityReference("role", context.SecurityManager.SystemAdministratorRoleId) }); | ||||||
|
|
||||||
| // Set caller (no impersonation) | ||||||
| context.CallerProperties.CallerId = new EntityReference("systemuser", adminUserId); | ||||||
| context.CallerProperties.ImpersonatedUserId = null; // Explicitly no impersonation | ||||||
|
|
||||||
| // Act - Create an account | ||||||
| var accountId = Guid.NewGuid(); | ||||||
| var account = new Entity("account") | ||||||
| { | ||||||
| Id = accountId, | ||||||
| ["name"] = "Test Account" | ||||||
| }; | ||||||
| service.Create(account); | ||||||
|
|
||||||
| // Assert - createdonbehalfof should NOT be set | ||||||
| var retrieved = service.Retrieve("account", accountId, new Microsoft.Xrm.Sdk.Query.ColumnSet(true)); | ||||||
| Assert.False(retrieved.Contains("createdonbehalfof")); | ||||||
|
||||||
| Assert.False(retrieved.Contains("createdonbehalfof")); | |
| Assert.False(retrieved.Contains("createdonbehalfby")); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tests assert the non-existent attribute createdonbehalfof. Dataverse uses createdonbehalfby. Update the attribute name and assertions to createdonbehalfby to reflect the actual field.