【问题标题】:Proper permission management when using CQRS使用 CQRS 时的适当权限管理
【发布时间】:2019-09-22 04:28:44
【问题描述】:

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

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

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 的实现非常复杂,可以运行许多不同的查询和命令。

  • 每个命令或查询都可以在系统的其他地方重复使用。

  • 每个命令都有一个 CommandHandler

  • 每个查询都有一个 QueryHandler

例子:

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 个命令的系统进行一些复杂的修改,最后,您无法完成整个事务,因为您无法运行最后一个命令。在这种情况下,需要进行复杂的回滚。

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

我的第一个想法是创建一个名为让我们说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
        }           
    }

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

权限检查应该只发生一次,我说得对吗? 如果是 - 在您想运行命令修改数据库然后将一些数据返回给客户端的情况下如何进行权限检查? 在这种情况下,您需要进行 2 次权限检查... 当您修改运行命令的数据库然后由于缺少某些权限而无法运行仅读取数据库的查询时,可能存在理论上的情况。如果系统很大并且有数百个 不同的权限,甚至良好的单元测试覆盖率都可能失败。

我的第二个想法是在命令和查询之上创建某种包装器或额外层,并在那里进行权限检查 但不知道如何实现。

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

【问题讨论】:

    标签: c# asp.net-mvc domain-driven-design cqrs


    【解决方案1】:

    如果您有某种流程需要多个命令/服务调用来执行该流程,那么这是 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 接口的实现来控制向用户显示哪些按钮/功能等。

    【讨论】:

      【解决方案2】:

      没有“正确的方法”,但我建议您可以从以下角度接近解决方案。

      在你的名字中使用Controller这个词并返回Ok()让我明白你正在处理一个http请求。但是内部发生的事情是业务用例的一部分,与 http 无关。所以,你最好弄点 Onion-ish 并引入一个(业务)应用层。

      这样,您的 http 控制器将负责: 1) 将 create tenancy http 请求解析为 create tenancy 业务请求 - 即在没有任何基础设施条款的域语言方面的请求对象模型。 2) 将业务响应格式化为http响应,包括将业务错误转换为http错误。

      因此,您进入应用层的是一个业务创建租赁请求。但这还不是命令。我不记得来源,但有人曾经说过,该命令应该是域内部的。它不能来自外部。您可以将命令视为决定是否更改应用程序状态所必需的综合对象模型。因此,我的建议是,在您的业务应用层中,您不仅要根据业务请求构建命令,还要根据所有这些查询的结果,包括对必要权限读取模型的查询。

      接下来,您可能有一个系统的单独决策业务核心,它接受一个包含所有综合数据的命令(一个值对象),应用一个纯决策函数并返回一个决策,也是一个值对象(事件或拒绝),再次包含从命令计算的所有必要数据。

      然后,当您的业务应用程序层收到决定时,它可以执行它,写入事件存储或存储库,记录、触发事件并最终对控制器产生业务响应。

      在大多数情况下,您可以接受这个单步决策过程。如果它需要不止一个步骤 - 也许这是重新考虑业务流程的提示,因为它对于单个 http 请求处理来说太复杂了。

      这样您将在处理命令之前获得所有权限。因此,您的业务核心将能够决定这些权限是否足以继续进行。它还可以使决策逻辑更加可测试,因此更加可靠。因为它是任何计算流程分支都应该测试的主要部分。

      请记住,这种方法倾向于最终的一致性,这在分布式系统中无论如何都是如此。但是,如果与单个数据库交互,您可以在单个事务中运行应用层代码。不过,我想你无论如何都要处理最终的一致性。

      希望这会有所帮助。

      【讨论】:

      • “我不记得来源了,但有人曾经说过,该命令应该是域内部的。它不能来自外部。”这是非常有趣的评论。如果你记得出处,如果你能把它写在这里,我将不胜感激:)
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-10-19
      • 1970-01-01
      • 1970-01-01
      • 2016-10-05
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多