Правильное управление разрешениями при использовании CQRS - PullRequest
1 голос
/ 03 мая 2019

Я использую разделение командных запросов в моей системе.

Для описания проблемы давайте начнем с примера. Допустим, у нас есть следующий код:

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

* * 1 022 Пример: * 1 023 *
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, которая реализована в действии контроллера в приведенном выше примере?

Ответы [ 2 ]

0 голосов
/ 16 мая 2019

В ситуации, когда у вас есть какой-то процесс, для выполнения которого требуется несколько команд / сервисных вызовов, тогда это идеальный кандидат на DomainService.

Служба DomainService по определению имеет некоторое знание предметной области и используется для облегчения процесса, который взаимодействует с несколькими агрегатами / службами.

В этом случае я бы хотел, чтобы ваше действие контроллера вызвало команду CQRS Command / CommandHandler. Этот CommandHandler примет доменную службу как одну зависимость. Тогда CommandHandler несет единоличную ответственность за вызов метода Domain Service.

Это означает, что ваш процесс CreateTenancy содержится в одном месте, DomainService.

Обычно мои CommandHandlers просто вызывают в сервисные методы. Поэтому DomainService может вызывать несколько служб для выполнения своих функций, а не вызывать несколько CommandHandlers. Я рассматриваю обработчики команд как фасад, через который мои контроллеры могут получить доступ к домену.

Когда речь идет о разрешениях, я обычно сначала решаю, является ли авторизация пользователей для выполнения процесса проблемой Домена. Если это так, я обычно создаю интерфейс для описания прав пользователей. А также, я обычно создаю интерфейс для этого конкретного ограниченного контекста, в котором я работаю. Так что в этом случае у вас может быть что-то вроде:

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.

Использование описанного выше метода может означать, что у вас немного больше кода и несколько интерфейсов разрешений, но это означает, что любые определяемые вами разрешения относятся к ограниченному контексту, в котором вы работаете. Я чувствую, что это лучше, чем иметь интерфейс IUserPermissions всей доменной модели, который может определять методы, которые не имеют отношения к делу и / или сбивают с толку в ограниченном контексте Tenancy.

Это означает, что вы можете проверять права пользователей в ваших экземплярах QueryValidator или CommandValidator. И, конечно же, вы можете использовать реализацию интерфейсов IPermission на уровне пользовательского интерфейса, чтобы контролировать, какие кнопки / функции и т. Д. Отображаются пользователю.

0 голосов
/ 03 мая 2019

«Правильного пути» не существует, но я бы посоветовал вам подойти к решению под следующим углом:

Использование слова Controller в ваших именах и возврат Ok() позволяетЯ понимаю, что вы обрабатываете HTTP-запрос.Но то, что происходит внутри, является частью бизнес-сценария, который не имеет ничего общего с http.Итак, вам лучше взять Onion-ish и представить (бизнес) прикладной уровень.

Таким образом, ваш http-контроллер будет отвечать за: 1) Разбор http-запроса на создание арендатора в бизнес-запрос на создание арендатора - т.е. объектную модель запроса с точки зрения языка домена, лишенного каких-либо инфраструктурных терминов.2) Форматирование бизнес-ответа в http-ответ, включая преобразование бизнес-ошибок в http-ошибки.

Итак, что вы получаете при входе на уровень приложения, так это запрос на создание бизнеса.Но это еще не команда.Я не могу вспомнить источник, но кто-то однажды сказал, что эта команда должна быть внутренней по отношению к домену.Это не может прийти извне.Команду можно считать всеобъемлющей объектной моделью, необходимой для принятия решения об изменении состояния приложения.Итак, я предлагаю, чтобы на уровне вашего бизнес-приложения вы строили команду не только из бизнес-запроса, но и из результатов всех этих запросов, включая запросы к необходимым моделям разрешения на чтение.

Далее у вас может бытьотдельное бизнес-ядро системы принятия решений, которое принимает команду (объект значения) со всеми исчерпывающими данными, применяет чистую функцию принятия решения и возвращает решение, а также объект значения (событие или отклонение), содержащий, опять же,все необходимые данные рассчитываются с помощью команды.

Затем, когда уровень вашего бизнес-приложения получает решение, он может выполнить его, записав в хранилища событий или репозитории, ведение журнала, запуск событий и, в конечном счете, создание бизнес-ответа наконтроллер.

В большинстве случаев вы будете в порядке с этим одношаговым процессом принятия решений.Если требуется более одного шага - возможно, это подсказка для пересмотра бизнес-потока, потому что он становится слишком сложным для обработки одного http-запроса.

Таким образом, вы получите все разрешения перед обработкой команды.,Таким образом, ядро ​​вашего бизнеса сможет принять решение, достаточны ли эти разрешения для продолжения работы.Это также может сделать логику принятия решений более проверяемой и, следовательно, надежной.Потому что это основная часть, которая должна тестироваться в любой ветви потока вычислений.

Имейте в виду, что этот подход склоняется к возможной согласованности, которая в любом случае имеется в распределенной системе.Хотя при взаимодействии с одной базой данных вы можете запустить код уровня приложения в одной транзакции.Я полагаю, однако, что вы все равно имеете дело с возможной последовательностью.

Надеюсь, это поможет.

...