ASP.NET 样板:什么是四眼原则的最佳解决方案

ASP.NET Boilerplate: What is the best solution for Four eyes principle

我想将 4 眼原则添加到 ASP.NET 样板框架。这意味着对角色、用户等的每项更改在应用到系统之前都需要(由另一位管理员)批准。我已经搜索了一段时间但没有答案。那么这个流程的最佳解决方案是什么?

我可以创建与 Abp 表(dbo.AbpUser_Temp 等)相同的表,并且所有更改都将存储在这些表中吗?有没有更好的解决办法?

示例:在应用程序中,Admin1 创建了一个名为 User1 的用户。但是这个用户在Admin2批准之前不能登录应用程序。

用于开发

  1. 分开的暂存和生产环境。在一个机器上进行开发、测试、审查,然后部署到生产机器上。简单、有效且与语言无关的建议。

自 ASP.NET 包含样板框架 Entity Framework。您还可以利用迁移。

  1. 完成开发工作并要求您 "update-database" 后,您的 SOP 应该是让管理员审查将要提交的(相对简单的)迁移。

希望对您有所帮助。

申请流程

可能有很多方法可以实际实现这一点,所以我将介绍一个简单的方法来让您的想法流畅,但请记住:您需要实现两人完整性的方式必须适合您的操作程序应该如何工作,而不是相反。开发不驱动业务运营,业务用例驱动开发。

  1. 扩展现有 Identity* classes。示例:ApplicationUser class(可能命名不同,但它派生自 IdentityUser

    • 创建 2 个必须且只能由管理员'on'
    • 设置的标志(布尔字段)
    • 单个管理员只能打开 1 个标志。 (这意味着您还必须存储哪个管理员打开了哪个标志。)
    • 标志可以存储在现有的 Abp* table 中,或者您可以创建一个新的 table
    • 添加逻辑,除非这两个标志都打开,否则不允许用户登录。
    • 示例:default IdentityUserRole 已识别并注册,但无法登录。一旦两个管理员都打开标志,将用户 IdentityUserRole 提升为允许登录的角色。

简单的工作流程

Example: In the application, Admin1 has created a user named User1. But this user cannot login to the application until he was approved by Admin2.

像这样的简单工作流程可以通过 属性 和方法适当地处理:

public class User : AbpUser<User>
{
    public bool IsApproved { get; set; }

    public void Approve(User approver)
    {
        if (approver.Id != CreatorUserId)
        {
            IsApproved = true;
        }
    }
}

复杂的工作流程

像 "every change" 这样的复杂工作流可以代替 _Temp 表来执行此操作:

public abstract class ChangeBase : Entity<long>, IExtendableObject
{
    public string EntityTypeAssemblyQualifiedName { get; set; }

    public string EntityIdJsonString { get; set; }

    public long ProposerUserId { get; set; }

    public long? ApproverUserId { get; set; }

    public string ExtensionData { get; set; }
}

public class Change : ChangeBase
{
    [NotMapped]
    public Type EntityType => Type.GetType(EntityTypeAssemblyQualifiedName);

    [NotMapped]
    public object EntityId => JsonConvert.DeserializeObject(EntityIdJsonString, EntityHelper.GetPrimaryKeyType(EntityType));

    [NotMapped]
    public bool IsApproved => ApproverUserId.HasValue && ApproverUserId != ProposerUserId;

    [NotMapped]
    public IDictionary<string, string> ChangedPropertyValuePairs => JObject.Parse(ExtensionData).ToObject<Dictionary<string, string>>();

    public Change(EntityIdentifier changedEntityIdentifier, long proposerUserId, IDictionary<string, string> changedPropertyValuePairs)
    {
        EntityTypeAssemblyQualifiedName = changedEntityIdentifier.Type.AssemblyQualifiedName;
        EntityIdJsonString = changedEntityIdentifier.Id.ToJsonString();
        ProposerUserId = proposerUserId;
        ExtensionData = JObject.FromObject(changedPropertyValuePairs).ToString(Formatting.None);
    }

    public bool Approve(long approverUserId)
    {
        if (approverUserId != ProposerUserId)
        {
            ApproverUserId = approverUserId;
            return true;
        }

        return false;
    }
}

用法:

public class UserAppService // ...
{
    private readonly IRepository<Change, long> _changeRepository;

    public UserAppService(
        IRepository<User, long> repository,
        IRepository<Change, long> changeRepository) // : base(repository)
    {
        _changeRepository = changeRepository;
    }

    public void ChangeUserName(long userId, string newUserName)
    {
        // Validation, etc.

        var changedPropertyValuePairs = new Dictionary<string, string> {
            { nameof(User.UserName), newUserName }
        };

        var change = new Change(
            new EntityIdentifier(typeof(User), userId),
            AbpSession.GetUserId(),
            changedPropertyValuePairs
            );

        _changeRepository.Insert(change);
    }

    public void ApproveChange(long changeId)
    {
        // Validation, etc.

        var change = _changeRepository.Get(changeId);

        if (change.EntityType == typeof(User) && change.Approve(AbpSession.GetUserId()))
        {
            var user = Repository.Get((long)change.EntityId);
            var changedPropertyValuePairs = change.ChangedPropertyValuePairs;

            foreach (var changedProperty in changedPropertyValuePairs.Keys)
            {
                switch (changedProperty)
                {
                    case nameof(User.UserName):
                        user.UserName = changedPropertyValuePairs[changedProperty];
                        break;
                    // ...
                    default:
                        break;
                }
            }
        }
    }