Skip to content
This repository was archived by the owner on Dec 15, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,22 @@ public interface ICallerProperties
{
EntityReference CallerId { get; set; }
EntityReference BusinessUnitId { get; set; }

/// <summary>
/// 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
/// </summary>
EntityReference ImpersonatedUserId { get; set; }

/// <summary>
/// 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.
/// </summary>
EntityReference GetEffectiveUser();
}
}
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);
Comment on lines +178 to +182
Copy link

Copilot AI Oct 17, 2025

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.

Copilot uses AI. Check for mistakes.
}

[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"));
Copy link

Copilot AI Oct 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test checks createdonbehalfof which is not a standard field; the correct attribute is createdonbehalfby. Update the check to Assert.False(retrieved.Contains("createdonbehalfby")) to validate expected behavior.

Suggested change
Assert.False(retrieved.Contains("createdonbehalfof"));
Assert.False(retrieved.Contains("createdonbehalfby"));

Copilot uses AI. Check for mistakes.
}
}
}
27 changes: 27 additions & 0 deletions Fake4DataverseCore/Fake4Dataverse.Core/CallerProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,38 @@ public class CallerProperties : ICallerProperties
{
public EntityReference CallerId { get; set; }
public EntityReference BusinessUnitId { get; set; }

/// <summary>
/// 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.
/// </summary>
public EntityReference ImpersonatedUserId { get; set; }

public CallerProperties()
{
CallerId = new EntityReference("systemuser", Guid.NewGuid());
BusinessUnitId = new EntityReference("businessunit", Guid.NewGuid());
}

/// <summary>
/// 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.
/// </summary>
public EntityReference GetEffectiveUser()
{
// Ensure CallerId is never null
if (CallerId == null)
{
CallerId = new EntityReference("systemuser", Guid.NewGuid());
}

return ImpersonatedUserId ?? CallerId;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading
Loading