diff --git a/Fake4DataverseAbstractions/Fake4Dataverse.Abstractions/ICallerProperties.cs b/Fake4DataverseAbstractions/Fake4Dataverse.Abstractions/ICallerProperties.cs
index f2fa9033..604c5de4 100644
--- a/Fake4DataverseAbstractions/Fake4Dataverse.Abstractions/ICallerProperties.cs
+++ b/Fake4DataverseAbstractions/Fake4Dataverse.Abstractions/ICallerProperties.cs
@@ -6,5 +6,22 @@ public interface ICallerProperties
{
EntityReference CallerId { get; set; }
EntityReference BusinessUnitId { get; set; }
+
+ ///
+ /// Gets or sets the ID of the user to impersonate when making requests.
+ /// When set, this user's identity is used for security checks and audit fields,
+ /// while CallerId represents the actual calling user.
+ /// Reference: https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/impersonate-another-user-web-api
+ ///
+ EntityReference ImpersonatedUserId { get; set; }
+
+ ///
+ /// Gets the effective user for operations.
+ /// Returns ImpersonatedUserId if impersonation is active, otherwise CallerId.
+ /// This is the user identity used for security checks, audit fields, and ownership.
+ /// Reference: https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/impersonate-another-user-web-api
+ /// When impersonating, operations are performed as if the impersonated user made them.
+ ///
+ EntityReference GetEffectiveUser();
}
}
diff --git a/Fake4DataverseCore/Fake4Dataverse.Core.Tests/Security/ImpersonationTests.cs b/Fake4DataverseCore/Fake4Dataverse.Core.Tests/Security/ImpersonationTests.cs
new file mode 100644
index 00000000..5693502b
--- /dev/null
+++ b/Fake4DataverseCore/Fake4Dataverse.Core.Tests/Security/ImpersonationTests.cs
@@ -0,0 +1,277 @@
+using Fake4Dataverse.Security;
+using Microsoft.Xrm.Sdk;
+using System;
+using System.Linq;
+using Xunit;
+
+namespace Fake4Dataverse.Core.Tests.Security
+{
+ ///
+ /// 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.
+ ///
+ 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("createdby"));
+ Assert.Equal(targetUserId, retrieved.GetAttributeValue("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("createdonbehalfof"));
+ Assert.Equal(adminUserId, retrieved.GetAttributeValue("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(() => 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"));
+ }
+ }
+}
diff --git a/Fake4DataverseCore/Fake4Dataverse.Core/CallerProperties.cs b/Fake4DataverseCore/Fake4Dataverse.Core/CallerProperties.cs
index 0d1c7024..af1ea2c4 100644
--- a/Fake4DataverseCore/Fake4Dataverse.Core/CallerProperties.cs
+++ b/Fake4DataverseCore/Fake4Dataverse.Core/CallerProperties.cs
@@ -8,11 +8,38 @@ public class CallerProperties : ICallerProperties
{
public EntityReference CallerId { get; set; }
public EntityReference BusinessUnitId { get; set; }
+
+ ///
+ /// Gets or sets the ID of the user to impersonate when making requests.
+ /// When set, this user's identity is used for security checks and audit fields,
+ /// while CallerId represents the actual calling user.
+ /// Reference: https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/impersonate-another-user-web-api
+ /// The impersonating user must have the prvActOnBehalfOfAnotherUser privilege.
+ ///
+ public EntityReference ImpersonatedUserId { get; set; }
public CallerProperties()
{
CallerId = new EntityReference("systemuser", Guid.NewGuid());
BusinessUnitId = new EntityReference("businessunit", Guid.NewGuid());
}
+
+ ///
+ /// Gets the effective user ID for operations.
+ /// Returns ImpersonatedUserId if impersonation is active, otherwise CallerId.
+ /// This is the user identity used for security checks, audit fields, and ownership.
+ /// Reference: https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/impersonate-another-user-web-api
+ /// When impersonating, operations are performed as if the impersonated user made them.
+ ///
+ public EntityReference GetEffectiveUser()
+ {
+ // Ensure CallerId is never null
+ if (CallerId == null)
+ {
+ CallerId = new EntityReference("systemuser", Guid.NewGuid());
+ }
+
+ return ImpersonatedUserId ?? CallerId;
+ }
}
}
diff --git a/Fake4DataverseCore/Fake4Dataverse.Core/Security/Middleware/SecurityMiddleware.cs b/Fake4DataverseCore/Fake4Dataverse.Core/Security/Middleware/SecurityMiddleware.cs
index d939ccec..47349b8d 100644
--- a/Fake4DataverseCore/Fake4Dataverse.Core/Security/Middleware/SecurityMiddleware.cs
+++ b/Fake4DataverseCore/Fake4Dataverse.Core/Security/Middleware/SecurityMiddleware.cs
@@ -36,12 +36,44 @@ public static IMiddlewareBuilder AddSecurity(this IMiddlewareBuilder builder)
return next(context, request);
}
- // Check if user is System Administrator - they bypass all security
- var callerId = context.CallerProperties.CallerId;
+ // Validate impersonation if active (must be done before sys admin bypass)
+ // Reference: https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/impersonate-another-user-web-api
+ // The calling user must have the prvActOnBehalfOfAnotherUser privilege to impersonate another user.
+ // Even System Administrators need to perform this check on the CALLING user, not the impersonated user.
+ if (context.CallerProperties.ImpersonatedUserId != null)
+ {
+ var callerId = context.CallerProperties.CallerId;
+ if (callerId == null)
+ {
+ throw new UnauthorizedAccessException("Cannot impersonate without a calling user context.");
+ }
+
+ // Check if the CALLING user (not impersonated) is System Administrator or has the privilege
+ bool isCallerSystemAdmin = context.SecurityManager.IsSystemAdministrator(callerId.Id);
+
+ if (!isCallerSystemAdmin)
+ {
+ // Check if the calling user has the prvActOnBehalfOfAnotherUser privilege
+ var hasPrivilege = context.SecurityManager.PrivilegeManager.HasPrivilege(
+ callerId.Id,
+ PrivilegeManager.ActOnBehalfOfAnotherUserPrivilege,
+ PrivilegeManager.PrivilegeDepthGlobal);
+
+ if (!hasPrivilege)
+ {
+ throw new UnauthorizedAccessException(
+ $"User {callerId.Id} does not have the '{PrivilegeManager.ActOnBehalfOfAnotherUserPrivilege}' privilege required for impersonation.");
+ }
+ }
+ }
+
+ // Check if EFFECTIVE user is System Administrator - they bypass all OTHER security
+ // The effective user is the impersonated user if impersonating, otherwise the caller
+ var effectiveUser = context.CallerProperties.ImpersonatedUserId ?? context.CallerProperties.CallerId;
- if (callerId != null && callerId.LogicalName == "systemuser")
+ if (effectiveUser != null && effectiveUser.LogicalName == "systemuser")
{
- if (context.SecurityManager.IsSystemAdministrator(callerId.Id))
+ if (context.SecurityManager.IsSystemAdministrator(effectiveUser.Id))
{
// System Administrators bypass all security checks
return next(context, request);
diff --git a/Fake4DataverseCore/Fake4Dataverse.Core/Security/PrivilegeManager.cs b/Fake4DataverseCore/Fake4Dataverse.Core/Security/PrivilegeManager.cs
index b524b4b9..cb465346 100644
--- a/Fake4DataverseCore/Fake4Dataverse.Core/Security/PrivilegeManager.cs
+++ b/Fake4DataverseCore/Fake4Dataverse.Core/Security/PrivilegeManager.cs
@@ -26,6 +26,12 @@ public class PrivilegeManager : IPrivilegeManager
public const int PrivilegeDepthLocal = 2; // User's business unit
public const int PrivilegeDepthDeep = 4; // User's business unit and child business units
public const int PrivilegeDepthGlobal = 8; // Organization-wide
+
+ // Special privilege names
+ // Reference: https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/impersonate-another-user-web-api
+ // The prvActOnBehalfOfAnotherUser privilege is required to impersonate other users.
+ // This privilege is typically granted to the System Administrator role.
+ public const string ActOnBehalfOfAnotherUserPrivilege = "prvActOnBehalfOfAnotherUser";
public PrivilegeManager(IXrmFakedContext context)
{
diff --git a/Fake4DataverseCore/Fake4Dataverse.Core/Services/EntityInitializer/DefaultEntityInitializerService.cs b/Fake4DataverseCore/Fake4Dataverse.Core/Services/EntityInitializer/DefaultEntityInitializerService.cs
index 605a8054..8adb3378 100644
--- a/Fake4DataverseCore/Fake4Dataverse.Core/Services/EntityInitializer/DefaultEntityInitializerService.cs
+++ b/Fake4DataverseCore/Fake4Dataverse.Core/Services/EntityInitializer/DefaultEntityInitializerService.cs
@@ -44,6 +44,17 @@ public Entity Initialize(Entity e, Guid gCallerId, XrmFakedContext ctx, bool isM
}
var CallerId = new EntityReference("systemuser", gCallerId); //Create a new instance by default
+
+ // Handle impersonation for audit fields
+ // Reference: https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/impersonate-another-user-web-api
+ // When impersonating:
+ // - createdby/modifiedby = impersonated user (passed in as gCallerId - the effective user)
+ // - createdonbehalfof/modifiedonbehalfof = actual calling user (from CallerProperties.CallerId)
+ var callerProperties = ctx.CallerProperties as CallerProperties;
+ var isImpersonating = callerProperties?.ImpersonatedUserId != null;
+
+ // The gCallerId parameter already represents the effective user (impersonated or actual caller)
+ var effectiveUser = CallerId;
var now = DateTime.UtcNow;
@@ -56,9 +67,19 @@ public Entity Initialize(Entity e, Guid gCallerId, XrmFakedContext ctx, bool isM
}
e.SetValueIfEmpty("modifiedon", now);
- e.SetValueIfEmpty("createdby", CallerId);
- e.SetValueIfEmpty("modifiedby", CallerId);
- e.SetValueIfEmpty("ownerid", CallerId);
+ e.SetValueIfEmpty("createdby", effectiveUser);
+ e.SetValueIfEmpty("modifiedby", effectiveUser);
+ e.SetValueIfEmpty("ownerid", effectiveUser);
+
+ // Set createdonbehalfof when impersonating
+ // Reference: https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/impersonate-another-user-web-api
+ // The createdonbehalfof field stores the actual user who initiated the operation (the impersonator)
+ if (isImpersonating && callerProperties?.CallerId != null)
+ {
+ e.SetValueIfEmpty("createdonbehalfof", callerProperties.CallerId);
+ e.SetValueIfEmpty("modifiedonbehalfof", callerProperties.CallerId);
+ }
+
e.SetValueIfEmpty("statecode", new OptionSetValue(0)); //Active by default
// Process auto number fields
diff --git a/Fake4DataverseCore/Fake4Dataverse.Core/XrmFakedContext.Audit.cs b/Fake4DataverseCore/Fake4Dataverse.Core/XrmFakedContext.Audit.cs
index 95e3cf67..079dfdaf 100644
--- a/Fake4DataverseCore/Fake4Dataverse.Core/XrmFakedContext.Audit.cs
+++ b/Fake4DataverseCore/Fake4Dataverse.Core/XrmFakedContext.Audit.cs
@@ -129,6 +129,9 @@ private bool ShouldAuditEntity(string entityLogicalName)
/// For Create operations in Dataverse:
/// - OldValue is an empty entity (the record didn't exist)
/// - NewValue contains the created entity's attributes
+ ///
+ /// When impersonating, the audit uses the impersonated user's ID.
+ /// Reference: https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/impersonate-another-user-web-api
///
internal void RecordCreateAudit(Entity entity)
{
@@ -138,7 +141,8 @@ internal void RecordCreateAudit(Entity entity)
}
var auditRepository = GetProperty();
- var userId = CallerProperties?.CallerId?.Id ?? Guid.Empty;
+ var effectiveUser = (CallerProperties as CallerProperties)?.GetEffectiveUser() ?? CallerProperties?.CallerId;
+ var userId = effectiveUser?.Id ?? Guid.Empty;
var objectRef = new EntityReference(entity.LogicalName, entity.Id);
// Pass the created entity so audit details can capture the new values
@@ -152,6 +156,9 @@ internal void RecordCreateAudit(Entity entity)
///
/// Records an audit entry for an Update operation
+ ///
+ /// When impersonating, the audit uses the impersonated user's ID.
+ /// Reference: https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/impersonate-another-user-web-api
///
internal void RecordUpdateAudit(Entity oldEntity, Entity newEntity)
{
@@ -161,7 +168,8 @@ internal void RecordUpdateAudit(Entity oldEntity, Entity newEntity)
}
var auditRepository = GetProperty();
- var userId = CallerProperties?.CallerId?.Id ?? Guid.Empty;
+ var effectiveUser = (CallerProperties as CallerProperties)?.GetEffectiveUser() ?? CallerProperties?.CallerId;
+ var userId = effectiveUser?.Id ?? Guid.Empty;
var objectRef = new EntityReference(newEntity.LogicalName, newEntity.Id);
// System-managed attributes that should not be audited as user changes
@@ -208,6 +216,9 @@ internal void RecordUpdateAudit(Entity oldEntity, Entity newEntity)
///
/// Records an audit entry for a Delete operation
+ ///
+ /// When impersonating, the audit uses the impersonated user's ID.
+ /// Reference: https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/impersonate-another-user-web-api
///
internal void RecordDeleteAudit(EntityReference entityRef)
{
@@ -217,7 +228,8 @@ internal void RecordDeleteAudit(EntityReference entityRef)
}
var auditRepository = GetProperty();
- var userId = CallerProperties?.CallerId?.Id ?? Guid.Empty;
+ var effectiveUser = (CallerProperties as CallerProperties)?.GetEffectiveUser() ?? CallerProperties?.CallerId;
+ var userId = effectiveUser?.Id ?? Guid.Empty;
auditRepository.CreateAuditRecord(
AuditAction.Delete,
diff --git a/Fake4DataverseCore/Fake4Dataverse.Core/XrmFakedContext.Crud.cs b/Fake4DataverseCore/Fake4Dataverse.Core/XrmFakedContext.Crud.cs
index 78cac71f..86c5806e 100644
--- a/Fake4DataverseCore/Fake4Dataverse.Core/XrmFakedContext.Crud.cs
+++ b/Fake4DataverseCore/Fake4Dataverse.Core/XrmFakedContext.Crud.cs
@@ -392,11 +392,12 @@ public void DeleteEntity(EntityReference er)
public void AddEntityDefaultAttributes(Entity e)
{
- // Ensure we have a valid caller
- if ( CallerProperties?.CallerId == null)
+ // Add createdon, modifiedon, createdby, modifiedby properties
+ // Ensure CallerProperties.CallerId is set if not already
+ if (CallerProperties != null && CallerProperties.CallerId == null)
{
- CallerProperties.CallerId = new EntityReference("systemuser", Guid.NewGuid()); // Create a new instance by default
- }
+ CallerProperties.CallerId = new EntityReference("systemuser", Guid.NewGuid());
+
// Ensure the systemuser entity exists for the caller
var systemUserLock = _entityLocks.GetOrAdd("systemuser", _ => new object());
@@ -405,13 +406,26 @@ public void AddEntityDefaultAttributes(Entity e)
var systemUserCollection = Data.GetOrAdd("systemuser", _ => new Dictionary());
if (!systemUserCollection.ContainsKey( CallerProperties.CallerId.Id))
{
- systemUserCollection.Add( CallerProperties.CallerId.Id, new Entity("systemuser") { Id = CallerProperties.CallerId.Id });
+ var systemUserLock = _entityLocks.GetOrAdd("systemuser", _ => new object());
+ lock (systemUserLock)
+ {
+ var systemUserCollection = Data.GetOrAdd("systemuser", _ => new Dictionary());
+ if (!systemUserCollection.ContainsKey(CallerProperties.CallerId.Id))
+ {
+ systemUserCollection.Add(CallerProperties.CallerId.Id, new Entity("systemuser") { Id = CallerProperties.CallerId.Id });
+ }
+ }
}
}
-
+
var isManyToManyRelationshipEntity = e.LogicalName != null && this._relationships.ContainsKey(e.LogicalName);
- EntityInitializerService.Initialize(e, CallerProperties.CallerId.Id, this, isManyToManyRelationshipEntity);
+ // Get the effective user ID for entity initialization
+ // When impersonating, use the impersonated user's ID, otherwise use the caller's ID
+ var effectiveUser = CallerProperties?.GetEffectiveUser();
+ var effectiveUserId = effectiveUser?.Id ?? Guid.Empty;
+
+ EntityInitializerService.Initialize(e, effectiveUserId, this, isManyToManyRelationshipEntity);
}
protected void ValidateEntity(Entity e)
diff --git a/docs/README.md b/docs/README.md
index b41b9a78..f3c4e710 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -22,6 +22,7 @@ Welcome to the Fake4Dataverse documentation! This testing framework allows you t
- [Testing Workflows](./usage/testing-workflows.md) - Custom workflow activity testing
- **[Security Model](./usage/security-model.md)** - Complete Dataverse security implementation ✅ **NEW**
- [Security and Permissions](./usage/security-permissions.md) - Testing security roles and access
+- **[Impersonation](./usage/impersonation.md)** - Perform operations on behalf of other users ✅ **NEW**
- [ExecuteMultiple and Transactions](./usage/batch-operations.md) - Batch operations and transactions
- [Auto Number Fields](./usage/auto-number-fields.md) - Auto-generated field values ✅ **NEW**
- [Metadata Validation](./usage/metadata-validation.md) - IsValidForCreate/Update/Read enforcement ✅ **NEW**
@@ -43,6 +44,7 @@ Welcome to the Fake4Dataverse documentation! This testing framework allows you t
### 🌐 [Network Testing](.)
- [Fake4DataverseService](./service.md) - Network-accessible SOAP/WCF service for integration testing ✅ **NEW**
- [REST/OData API Endpoints](./rest-api.md) - OData v4.0 endpoints with advanced query support ✅ **NEW**
+- **[Model-Driven App Interface](./usage/mda-interface.md)** - Web UI for visual testing and user impersonation ✅ **NEW**
### 🏗️ [Architecture & Planning](.)
- [Testing Guide](./TESTING_GUIDE.md) - How to run all tests in the repository ✅ **NEW**
@@ -89,9 +91,11 @@ Welcome to the Fake4Dataverse documentation! This testing framework allows you t
- **Understand the architecture**: Read [Middleware Architecture](./concepts/middleware.md)
- **Test security with full Dataverse model**: See [Security Model](./usage/security-model.md) ✅ **NEW**
- **Test basic security**: See [Security and Permissions](./usage/security-permissions.md)
+- **Impersonate another user**: See [Impersonation](./usage/impersonation.md)
- **Migrate from FakeXrmEasy**: Check the [Migration Guides](./migration/)
- **Find supported messages**: Browse [Message Executors](./messages/)
- **Do integration testing**: Use [Fake4DataverseService](./service.md) for network-accessible testing
+- **Use the web interface**: See [Model-Driven App Interface](./usage/mda-interface.md) for visual testing
## Quick Example
diff --git a/docs/usage/impersonation.md b/docs/usage/impersonation.md
new file mode 100644
index 00000000..7609a7a1
--- /dev/null
+++ b/docs/usage/impersonation.md
@@ -0,0 +1,332 @@
+# Impersonation
+
+**Implementation Date:** January 2025
+**Issue:** [#116](https://github.com/rnwood/Fake4Dataverse/issues/116)
+
+## Overview
+
+Impersonation allows a user with appropriate privileges to perform operations on behalf of another user. This is useful for:
+- Service accounts performing actions for end users
+- Administrators troubleshooting user-specific issues
+- Integration scenarios where one system acts on behalf of users
+
+**Reference:** [Microsoft Learn - Impersonate another user](https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/impersonate-another-user-web-api)
+
+## How Impersonation Works
+
+When impersonating:
+1. The **calling user** (impersonator) must have the `prvActOnBehalfOfAnotherUser` privilege
+2. Operations are performed as if the **impersonated user** made them
+3. Security checks use the impersonated user's permissions
+4. Audit fields reflect both users:
+ - `createdby`/`modifiedby` = impersonated user
+ - `createdonbehalfof`/`modifiedonbehalfof` = calling user (impersonator)
+
+## Core API Usage
+
+### Setting Up Impersonation
+
+```csharp
+var context = new XrmFakedContext();
+context.SecurityConfiguration.SecurityEnabled = true;
+var service = context.GetOrganizationService();
+
+// Create users
+var adminUserId = Guid.NewGuid();
+var targetUserId = Guid.NewGuid();
+
+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 to admin
+service.Associate("systemuser", adminUserId,
+ new Relationship("systemuserroles_association"),
+ new EntityReferenceCollection { new EntityReference("role", context.SecurityManager.SystemAdministratorRoleId) });
+
+// Set up impersonation
+context.CallerProperties.CallerId = new EntityReference("systemuser", adminUserId);
+context.CallerProperties.ImpersonatedUserId = new EntityReference("systemuser", targetUserId);
+
+// All subsequent operations are performed as targetUser
+var account = new Entity("account") { ["name"] = "Contoso Ltd" };
+var accountId = service.Create(account);
+
+// The account is created as if targetUser created it
+var retrieved = service.Retrieve("account", accountId, new ColumnSet(true));
+Assert.Equal(targetUserId, retrieved.GetAttributeValue("createdby").Id);
+Assert.Equal(adminUserId, retrieved.GetAttributeValue("createdonbehalfof").Id);
+```
+
+### Clearing Impersonation
+
+```csharp
+// Stop impersonating
+context.CallerProperties.ImpersonatedUserId = null;
+
+// Operations now performed as the actual calling user
+```
+
+## Privilege Requirements
+
+The calling user must have one of the following:
+1. **System Administrator role** (has `prvActOnBehalfOfAnotherUser` implicitly)
+2. **Custom role** with the `prvActOnBehalfOfAnotherUser` privilege assigned
+
+### Granting Impersonation Privilege
+
+```csharp
+// Create the impersonation privilege if it doesn't exist
+var impersonationPrivilege = new Entity("privilege")
+{
+ Id = Guid.NewGuid(),
+ ["name"] = "prvActOnBehalfOfAnotherUser",
+ ["accessright"] = 0, // Special privilege (not entity-specific)
+ ["privilegeid"] = Guid.NewGuid()
+};
+context.AddEntity(impersonationPrivilege);
+
+// Assign to a role
+var rolePrivilege = new Entity("roleprivileges")
+{
+ Id = Guid.NewGuid(),
+ ["roleid"] = new EntityReference("role", customRoleId),
+ ["privilegeid"] = new EntityReference("privilege", impersonationPrivilege.Id),
+ ["privilegedepthmask"] = 8 // Global depth
+};
+context.AddEntity(rolePrivilege);
+```
+
+## Service Layer Usage
+
+### Web API / REST (HTTP Header)
+
+When using Fake4DataverseService with HTTP/REST endpoints, set the `MSCRMCallerID` header:
+
+```csharp
+using (var client = new HttpClient())
+{
+ client.BaseAddress = new Uri("http://localhost:5000/api/data/v9.2/");
+
+ // Set the impersonation header
+ client.DefaultRequestHeaders.Add("MSCRMCallerID", targetUserId.ToString());
+
+ // Create an account - operation performed as targetUserId
+ var account = new { name = "Contoso Ltd" };
+ var response = await client.PostAsJsonAsync("accounts", account);
+}
+```
+
+### SOAP / WCF (SOAP Header)
+
+When using the SOAP endpoint, set the `CallerObjectId` in the SOAP header:
+
+```csharp
+var binding = new BasicHttpBinding();
+var endpoint = new EndpointAddress("http://localhost:5000/XRMServices/2011/Organization.svc");
+var factory = new ChannelFactory(binding, endpoint);
+
+using (var scope = new OperationContextScope((IContextChannel)service))
+{
+ // Add the impersonation header
+ var header = MessageHeader.CreateHeader(
+ "CallerId",
+ "http://schemas.microsoft.com/xrm/2011/Contracts",
+ targetUserId);
+ OperationContext.Current.OutgoingMessageHeaders.Add(header);
+
+ // Create an account - operation performed as targetUserId
+ var account = new Entity("account") { ["name"] = "Contoso Ltd" };
+ var accountId = service.Create(account);
+}
+```
+
+## Security Behavior
+
+### Permission Checks
+
+When impersonating, **all security checks use the impersonated user's permissions**:
+
+```csharp
+// Admin impersonates a user with limited permissions
+context.CallerProperties.CallerId = new EntityReference("systemuser", adminUserId);
+context.CallerProperties.ImpersonatedUserId = new EntityReference("systemuser", limitedUserId);
+
+// This will FAIL if limitedUser doesn't have create privilege for account
+// Even though adminUser is a System Administrator
+var account = new Entity("account") { ["name"] = "Test" };
+service.Create(account); // Throws UnauthorizedAccessException
+```
+
+### Impersonation Validation
+
+If the calling user lacks the impersonation privilege:
+
+```csharp
+// Regular user without impersonation privilege
+context.CallerProperties.CallerId = new EntityReference("systemuser", regularUserId);
+context.CallerProperties.ImpersonatedUserId = new EntityReference("systemuser", targetUserId);
+
+// This will throw UnauthorizedAccessException
+service.Create(account);
+// Error: "User {regularUserId} does not have the 'prvActOnBehalfOfAnotherUser' privilege"
+```
+
+## Audit Trail
+
+Impersonation creates a complete audit trail:
+
+```csharp
+// After creating with impersonation
+var account = service.Retrieve("account", accountId, new ColumnSet(true));
+
+// Fields show both users
+Console.WriteLine($"Created by: {account.GetAttributeValue("createdby").Id}");
+// Output: Created by: {targetUserId}
+
+Console.WriteLine($"Created on behalf of: {account.GetAttributeValue("createdonbehalfof").Id}");
+// Output: Created on behalf of: {adminUserId}
+
+// Audit records also reflect the impersonated user
+var auditRecords = context.AuditRepository.GetAuditRecordsForEntity("account", accountId);
+var auditUserId = auditRecords.First().GetAttributeValue("userid").Id;
+Assert.Equal(targetUserId, auditUserId);
+```
+
+## Common Patterns
+
+### Bulk Operations
+
+```csharp
+// Impersonate once, perform multiple operations
+context.CallerProperties.CallerId = adminRef;
+context.CallerProperties.ImpersonatedUserId = userRef;
+
+foreach (var item in items)
+{
+ var entity = new Entity("account") { ["name"] = item.Name };
+ service.Create(entity);
+}
+
+// Clear impersonation
+context.CallerProperties.ImpersonatedUserId = null;
+```
+
+### Plugin Context
+
+```csharp
+// In a plugin test, set impersonation for the plugin execution
+var pluginContext = context.GetDefaultPluginContext();
+pluginContext.InitiatingUserId = adminUserId;
+pluginContext.UserId = targetUserId; // The impersonated user
+
+// Execute plugin
+var plugin = new YourPlugin();
+plugin.Execute(pluginContext);
+```
+
+### Testing Impersonation Logic
+
+```csharp
+[Fact]
+public void Should_Respect_Impersonated_User_Permissions()
+{
+ // Arrange
+ var context = new XrmFakedContext();
+ context.SecurityConfiguration.SecurityEnabled = true;
+
+ var admin = CreateAdminUser(context);
+ var limitedUser = CreateUserWithLimitedPermissions(context);
+
+ context.CallerProperties.CallerId = admin.ToEntityReference();
+ context.CallerProperties.ImpersonatedUserId = limitedUser.ToEntityReference();
+
+ var service = context.GetOrganizationService();
+
+ // Act & Assert - Should fail due to limited user's permissions
+ var account = new Entity("account") { ["name"] = "Test" };
+ Assert.Throws(() => service.Create(account));
+}
+```
+
+## Key Differences from FakeXrmEasy v2
+
+**Important**: Fake4Dataverse's impersonation differs from FakeXrmEasy v2+ in the following ways:
+
+| Feature | FakeXrmEasy v2+ | Fake4Dataverse v4 |
+|---------|-----------------|-------------------|
+| Property Name | `CallerProperties.ImpersonationUserId` | `CallerProperties.ImpersonatedUserId` |
+| Interface Method | None | `ICallerProperties.GetEffectiveUser()` |
+| Privilege Check | Automatic | Explicit validation in SecurityMiddleware |
+| HTTP Header | Manual setup | Built-in middleware support |
+| SOAP Header | Manual setup | Built-in middleware support |
+
+## Best Practices
+
+1. **Always enable security** when testing impersonation:
+ ```csharp
+ context.SecurityConfiguration.SecurityEnabled = true;
+ ```
+
+2. **Clear impersonation** when no longer needed:
+ ```csharp
+ context.CallerProperties.ImpersonatedUserId = null;
+ ```
+
+3. **Test both success and failure** scenarios:
+ - Verify operations succeed with proper privileges
+ - Verify operations fail without proper privileges
+ - Verify audit fields are set correctly
+
+4. **Use meaningful user names** in tests:
+ ```csharp
+ var admin = new Entity("systemuser") { ["fullname"] = "Admin User" };
+ var target = new Entity("systemuser") { ["fullname"] = "Target User" };
+ ```
+
+## Troubleshooting
+
+### "User does not have the 'prvActOnBehalfOfAnotherUser' privilege"
+
+**Cause:** The calling user (impersonator) lacks the impersonation privilege.
+
+**Solution:** Assign System Administrator role or grant the specific privilege:
+```csharp
+service.Associate("systemuser", callerUserId,
+ new Relationship("systemuserroles_association"),
+ new EntityReferenceCollection { new EntityReference("role", sysAdminRoleId) });
+```
+
+### Operations Fail with Impersonation
+
+**Cause:** The impersonated user lacks necessary permissions.
+
+**Solution:** Grant appropriate privileges to the impersonated user or verify role assignments.
+
+### createdonbehalfof Not Being Set
+
+**Cause:** Impersonation is not active (ImpersonatedUserId is null).
+
+**Solution:** Ensure ImpersonatedUserId is set before the operation:
+```csharp
+context.CallerProperties.ImpersonatedUserId = targetUserRef;
+```
+
+## See Also
+
+- [Security Model](security-model.md) - Overview of Fake4Dataverse security
+- [Security Permissions](security-permissions.md) - Managing roles and privileges
+- [Audit](auditing.md) - Audit trail functionality
+- [Microsoft Learn - Impersonation](https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/impersonate-another-user-web-api)
diff --git a/docs/usage/mda-interface.md b/docs/usage/mda-interface.md
new file mode 100644
index 00000000..59028d15
--- /dev/null
+++ b/docs/usage/mda-interface.md
@@ -0,0 +1,341 @@
+# Model-Driven App (MDA) Interface
+
+**Implementation Date:** January 2025
+**Issue:** [#116](https://github.com/rnwood/Fake4Dataverse/issues/116)
+
+## Overview
+
+The Fake4DataverseService includes a web-based Model-Driven App (MDA) interface that provides a visual way to interact with the fake Dataverse instance. This is particularly useful for:
+- Manual testing and exploration of test data
+- Debugging issues during test development
+- Demonstrating test scenarios to stakeholders
+- Validating impersonation and security configurations
+
+## Accessing the MDA
+
+When Fake4DataverseService is running, the MDA is accessible at:
+
+```
+http://localhost:{port}/mda/
+```
+
+Default: `http://localhost:5000/mda/`
+
+## Key Features
+
+### 1. User Management and Impersonation
+
+The MDA header displays the current user context:
+
+- **User Avatar**: Shows the currently active user (or impersonated user)
+- **User Switcher**: Click the avatar to switch between users
+- **Impersonation Indicator**: Visual indicator when impersonating another user
+- **Real-time Update**: Operations immediately reflect the selected user context
+
+#### Using the User Switcher
+
+1. Click the user avatar in the top-right corner
+2. Select a user from the dropdown list
+3. All subsequent operations are performed as that user
+4. The interface refreshes to show data visible to the selected user
+
+#### Impersonation Workflow
+
+When you switch users in the MDA:
+1. The MDA sets the `MSCRMCallerID` HTTP header on all requests
+2. The service applies impersonation for the selected user
+3. Security checks use the impersonated user's permissions
+4. Created/modified records show the correct audit trail
+
+### 2. Entity Data Browsing
+
+- View all entities in the fake instance
+- Browse records with filtering and sorting
+- View entity metadata and relationships
+- Inspect field values including lookups and option sets
+
+### 3. Record Operations
+
+- **Create**: Add new records through forms
+- **Update**: Modify existing records
+- **Delete**: Remove records
+- **Associate**: Link records via relationships
+
+All operations respect the current user's security permissions.
+
+### 4. Security Visualization
+
+- See which records are visible to the current user
+- Test security role configurations
+- Verify business unit hierarchies
+- Validate sharing permissions
+
+## Setup and Configuration
+
+### Starting the Service with MDA
+
+```bash
+# Start with default settings
+fake4dataverse start --port 5000
+
+# Custom configuration
+fake4dataverse start --port 8080 --host 0.0.0.0
+```
+
+The MDA is automatically available at the `/mda/` endpoint.
+
+### Creating Initial Users
+
+When the service starts, it automatically creates a System Administrator user:
+
+```csharp
+// Automatically created on service startup
+var systemAdmin = new Entity("systemuser")
+{
+ Id = Guid.NewGuid(),
+ ["fullname"] = "System Administrator",
+ ["businessunitid"] = rootBusinessUnit.ToEntityReference()
+};
+```
+
+You can create additional users via:
+1. The MDA interface itself
+2. SOAP/OData API calls
+3. Initialization scripts
+
+### Configuring User List
+
+The user switcher displays all systemuser records in the instance. To add users:
+
+```csharp
+// Via OData API
+POST http://localhost:5000/api/data/v9.2/systemusers
+Content-Type: application/json
+
+{
+ "fullname": "Test User",
+ "businessunitid@odata.bind": "/businessunits({businessunitid})"
+}
+```
+
+Or through the MDA's user management interface.
+
+## Example Scenarios
+
+### Scenario 1: Testing Security Roles
+
+```
+1. Start Fake4DataverseService
+2. Open MDA in browser
+3. Create two users: "Admin" and "Sales Rep"
+4. Assign different security roles to each
+5. Create some account records as Admin
+6. Switch to Sales Rep user
+7. Observe which records are visible
+8. Try creating/updating records
+9. Verify security enforcement
+```
+
+### Scenario 2: Impersonation Testing
+
+```
+1. Create Admin user with System Administrator role
+2. Create Target user with limited permissions
+3. Switch to Admin user in MDA
+4. The backend automatically sets impersonation
+5. Create an account record
+6. Inspect the record to see:
+ - createdby = Admin
+ - createdonbehalfof = (not set, since not impersonating)
+7. In your application, set impersonation headers
+8. Create another record
+9. Inspect to see both users in audit fields
+```
+
+### Scenario 3: Multi-User Workflows
+
+```
+1. Create users for different roles (Sales, Manager, Admin)
+2. Switch between users to simulate workflow
+3. Sales creates an opportunity
+4. Manager reviews and updates
+5. Admin performs administrative tasks
+6. Verify audit trail shows correct user chain
+```
+
+## Technical Implementation
+
+### Architecture
+
+The MDA is a Next.js application embedded in the Fake4DataverseService:
+
+```
+┌─────────────────────────────────────┐
+│ Browser (MDA UI) │
+│ (Next.js + React) │
+└────────────┬────────────────────────┘
+ │ HTTP + MSCRMCallerID header
+ │
+┌────────────▼────────────────────────┐
+│ Fake4DataverseService │
+│ ┌──────────────────────────────┐ │
+│ │ Impersonation Middleware │ │
+│ │ (Extract MSCRMCallerID) │ │
+│ └──────────┬───────────────────┘ │
+│ │ │
+│ ┌──────────▼───────────────────┐ │
+│ │ OData/SOAP Endpoints │ │
+│ └──────────┬───────────────────┘ │
+└─────────────┼───────────────────────┘
+ │
+ ┌──────────▼───────────────────┐
+ │ Fake4DataverseCore │
+ │ (In-Memory Context) │
+ └──────────────────────────────┘
+```
+
+### HTTP Header Format
+
+When a user is selected in the MDA, all requests include:
+
+```http
+GET /api/data/v9.2/accounts HTTP/1.1
+Host: localhost:5000
+MSCRMCallerID: {userId}
+```
+
+The middleware extracts this header and sets `CallerProperties.ImpersonatedUserId`.
+
+### State Management
+
+The MDA maintains user selection in:
+1. Browser session storage
+2. React context
+3. HTTP headers on every request
+
+Switching users:
+1. Updates React state
+2. Stores selection in session
+3. Adds header to all subsequent requests
+4. Refreshes data view
+
+## Development and Testing
+
+### Running MDA in Development
+
+```bash
+cd Fake4DataverseService/Fake4Dataverse.Service/mda-app
+
+# Install dependencies
+npm ci
+
+# Run development server
+npm run dev
+
+# Run alongside service
+dotnet run --project ../Fake4Dataverse.Service.csproj
+```
+
+### MDA Tests
+
+```bash
+# Unit tests
+npm test
+
+# E2E tests
+npm run test:e2e
+```
+
+### Building for Production
+
+```bash
+# Build Next.js app
+npm run build
+
+# The build is embedded in the service package
+dotnet pack
+```
+
+## Troubleshooting
+
+### MDA Not Loading
+
+**Problem:** Navigating to `/mda/` shows 404 or blank page
+
+**Solutions:**
+- Ensure Fake4DataverseService is running
+- Check the correct port
+- Verify the MDA app was built: `mda-app/out/` should exist
+- Check console for JavaScript errors
+
+### User Switcher Empty
+
+**Problem:** No users appear in the switcher dropdown
+
+**Solutions:**
+- Create at least one systemuser record
+- Verify systemuser entity is not filtered
+- Check browser console for API errors
+- Ensure OData endpoint is accessible
+
+### Impersonation Not Working
+
+**Problem:** Operations don't reflect the selected user
+
+**Solutions:**
+- Verify the MSCRMCallerID header is being sent (check browser dev tools)
+- Ensure impersonation middleware is registered in service
+- Check security is enabled: `SecurityConfiguration.SecurityEnabled = true`
+- Verify user has necessary privileges
+
+### Permission Errors
+
+**Problem:** Operations fail with permission errors
+
+**Solutions:**
+- Verify the selected user has appropriate security roles
+- Check business unit hierarchy
+- Ensure privileges are assigned correctly
+- Use System Administrator for debugging
+
+## API Reference
+
+### Getting Available Users
+
+```http
+GET /api/data/v9.2/systemusers?$select=systemuserid,fullname
+```
+
+### Setting Current User
+
+```http
+GET /api/data/v9.2/accounts
+MSCRMCallerID: {userId}
+```
+
+### Getting Current User Context
+
+```http
+POST /api/data/v9.2/WhoAmI
+```
+
+Response includes the effective user when impersonating.
+
+## Best Practices
+
+1. **Create Meaningful Users**: Use descriptive names like "Admin User", "Sales Rep", "Manager"
+
+2. **Assign Proper Roles**: Give each test user appropriate security roles for realistic testing
+
+3. **Test Permission Boundaries**: Switch between users to verify security enforcement
+
+4. **Clear Data Between Tests**: Use the MDA to inspect data state between test runs
+
+5. **Document User Scenarios**: Maintain a list of test users and their purposes
+
+## See Also
+
+- [Impersonation](./impersonation.md) - Core impersonation concepts and API
+- [Security Model](./security-model.md) - Dataverse security implementation
+- [Fake4DataverseService](../service.md) - Service architecture and setup
+- [REST API](../rest-api.md) - OData endpoint reference