首页 > 解决方案 > 使用 CQRS 时的适当权限管理

问题描述

我在我的系统中使用命令查询分离。

为了描述这个问题,让我们从一个例子开始。假设我们有如下代码:

public class TenancyController : ControllerBase{
    public async Task<ActionResult> CreateTenancy(CreateTenancyRto rto){

      // 1. Run Blah1Command
      // 2. Run Blah2Command
      // 3. Run Bar1Query
      // 4. Run Blah3Command
      // 5. Run Bar2Query
      // ...
      // n. Run BlahNCommand
      // n+1. Run BarNQuery

      //example how to run a command in the system:
      var command = new UploadTemplatePackageCommand
      {
          Comment = package.Comment,
          Data = Request.Body,
          TemplatePackageId = id
      };
      await _commandDispatcher.DispatchAsync(command);

      return Ok();
    }
}

CreateTenancy有一个非常复杂的实现并运行许多不同的查询和命令。

例子:

public class UploadTemplatePackageCommandHandler : PermissionedCommandHandler<UploadTemplatePackageCommand>
    {
        //ctor

        protected override Task<IEnumerable<PermissionDemand>> GetPermissionDemandsAsync(UploadTemplatePackageCommand command) {
          //return list of demands
        }

        protected override async Task HandleCommandAsync(UploadTemplatePackageCommand command)
        {
          //some business logic
        }           
}

每次您尝试运行命令或查询时,都会进行权限检查。出现的问题CreateTenancy是当你运行 10 个命令时。有时您对前 9 个命令都有权限,但缺少运行最后一个命令的一些权限。在这种情况下,您可以对运行这 9 个命令的系统进行一些复杂的修改,最后,您无法完成整个事务,因为您无法运行最后一个命令。在这种情况下,需要进行复杂的回滚。

我相信在上面的例子中,权限检查应该只在整个事务开始时进行一次,但我不确定实现这一点的最佳方法是什么。

我的第一个想法是创建一个名为 let's say 的命令CreateTenancyCommand,在这个HandleCommandAsync地方,整个逻辑来自CreateTenancy(CreateTenancyRto rto) 所以它看起来像:

public class CreateTenancyCommand : PermissionedCommandHandler<UploadTemplatePackageCommand>
{
        //ctor

        protected override Task<IEnumerable<PermissionDemand>> GetPermissionDemandsAsync(UploadTemplatePackageCommand command) {
          //return list of demands
        }

        protected override async Task HandleCommandAsync(UploadTemplatePackageCommand command)
        {
          // 1. Run Blah1Command
          // 2. Run Blah2Command
          // 3. Run Bar1Query
          // 4. Run Blah3Command
          // 5. Run Bar2Query
          // ...
          // n. Run BlahNCommand
          // n+1. Run BarNQuery
        }           
    }

我不确定在另一个命令的命令处理程序中调用命令是否是一种好方法?我认为每个命令处理程序应该是独立的。

Am I right that the permission check should happen only once? If yes- how to do the permission check in the case when you want to run a command to modify the database and then return some data to the client? In such a case, you would need to do 2 permission checks... There can be a theoretical case when you modify the database running the command and then cannot run a query which only reads the database because you are missing some of the permissions. It can be very problematic for the developer to detect such a situation if the system is big and there are hundreds of different permissions and even the good unit tests coverage can fail.

My second idea is to create some kind of wrapper or extra layer above the commands and queries and do the permission check there but not sure how to implement it.

CreateTenancy在上述示例中的控制器操作中实现的所述事务中进行权限检查的正确方法是什么?

标签: c#asp.net-mvcdomain-driven-designcqrs

解决方案


在您有某种需要多个命令/服务调用来执行该过程的过程的情况下,这是 DomainService 的理想候选者。

根据定义,DomainService 是具有一些领域知识的服务,用于促进与多个聚合/服务交互的过程。

在这种情况下,我希望您的控制器操作调用 CQRS 命令/命令处理程序。该 CommandHandler 将域服务作为单个依赖项。然后,CommandHandler 只负责调用域服务方法。

这意味着您的 CreateTenancy 流程包含在一个地方,即 DomainService。

我通常让我的 CommandHandlers 简单地调用服务方法。因此,DomainService 可以调用多个服务来执行其功能,而不是调用多个 CommandHandler。我将命令处理程序视为我的控制器可以访问域的外观。

当涉及到权限时,我通常首先确定用户执行流程的权限是否是域问题。如果是这样,我通常会创建一个接口来描述用户权限。而且,我通常会为此创建一个特定于我正在工作的有界上下文的接口。所以在这种情况下,你可能会有类似的东西:

public interface ITenancyUserPermissions
{
     bool CanCreateTenancy(string userId);
}

然后我会让 ITenancyUserPermission 接口成为我的 CommandValidator 中的依赖项:

    public class CommandValidator : AbstractValidator<Command>
    {
        private ITenancyUserPermissions _permissions;

        public CommandValidator(ITenancyUserPermissions permissions)
        {
           _permissions = permissions;

            RuleFor(r => r).Must(HavePermissionToCreateTenancy).WithMessage("You do not have permission to create a tenancy.");
        }

        public bool HavePermissionToCreateTenancy(Command command)
        {
             return _permissions.CanCreateTenancy(command.UserId);
        }

    }

您说创建租户的权限取决于执行其他任务/命令的权限。那些其他命令将有自己的一组权限接口。然后最终在您的应用程序中,您将拥有这些接口的实现,例如:

public class UserPermissions : ITenancyUserPermissions, IBlah1Permissions, IBlah2Permissions
{

    public bool CanCreateTenancy(string userId)
    {
        return CanBlah1 && CanBlah2;
    }

    public bool CanBlah1(string userID)
    {
        return _authService.Can("Blah1", userID);            
    }

    public bool CanBlah2(string userID)
    {
        return _authService.Can("Blah2", userID);
    }
}

在我的例子中,我使用 ABAC 系统,将策略存储和处理为 XACML 文件。

使用上述方法可能意味着您有更多的代码和几个 Permissions 接口,但这确实意味着您定义的任何权限都特定于您正在工作的限界上下文。我觉得这比拥有一个域模型范围的 IUserPermissions 接口要好,该接口可能会定义不相关的方法,和/或在您的 Tenancy 有界上下文中混淆。

这意味着您可以在 QueryValidator 或 CommandValidator 实例中检查用户权限。当然,您可以在 UI 级别使用 IPermission 接口的实现来控制向用户显示哪些按钮/功能等。


推荐阅读