DDD как правильно разделить права и роли

Допустим, у меня есть приложение, которое позволяет пользователям создавать, назначать и редактировать задачи. Приложение создано для поддержки нескольких учетных записей клиентов. Это приложение имеет различные роли, но мы сосредоточимся на трех и их разрешениях, относящихся к конкретному действию и ресурсу (обновление текста в задаче) — администраторы (имеют разрешения на обновление текста во всех задачах во всех учетных записях), администраторы учетных записей (имеют разрешения на обновление текста в любой задаче, принадлежащей их учетным записям, но они могут принадлежать нескольким учетным записям) и пользователи учетной записи (имеют разрешения на обновление текста в задаче, только если эта задача назначена им).

Пример немного надуманный, а имена ролей слишком общие, но потерпите меня.

Цель здесь — попытаться найти аккуратный способ разделения ролей и разрешений, но кажется, что роли неизбежно привязаны к разрешениям (см. код ниже).

Возможно, разрешение должно быть просто task:updateText, но как тогда проверить роли? Могу ли я переместить свой блок switch (actor.type) в модели домена в службу домена и проверить там, связан ли пользователь с администратором, администратором учетной записи или пользователем учетной записи в этой конкретной учетной записи? Данные могут быть кэшированы, но администраторы учетных записей (и, возможно, другие пользователи) могут быть связаны с несколькими учетными записями, что означает, что предварительная загрузка этих данных может потребовать слишком много данных в контексте и может быть проблематичной, поскольку эти данные передаются между службами.

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

Каковы мои варианты здесь? Любое руководство будет высоко оценено.

class TaskApplicationService {
  constructor(private taskRepository: TaskRepository) { }
  async updateText({ taskId, text, accountId, context }: { taskId: string, text: string, accountId?: string, context: Context }) {
    let actor: Actor;
    const userId = context.user.id;
    // permissions follow pattern resource:action:qualifier
    if (await hasPermission('task:updateText:all')) {
      actor = await anAdmin({ userId });
    } else if (await hasPermission('task:updateText:account')) {
      actor = await anAccountAdmin({ accountId, userId });
    } else if (await hasPermission('task.updateText:assigned')) {
      actor = await anAccountUser({ accountId, userId });
    } else {
      throw new Error('not authorized');
    }

    const task = await this.taskRepository.findOne({ taskId });
    task.updateText({ text, actor });
    await this.taskRepository.save(task);
    // return TaskMapper.toDto(task);
  }
}

class TaskDomainModel {
  private props: {
    text: string,
    accountId: string,
    assignedAccountUserId: string;
  };

  get text(): string {
    return this.props.text;
  }

  updateText({ text, actor }: { text: string, actor: Actor }) {
    switch (actor.type) {
      case ActorType.ADMIN:
        break;
      case ActorType.ACCOUNT_ADMIN:
        assert(this.props.accountId === actor.tenantId);
        break;
      case ActorType.ACCOUNT_USER:
        assert(this.props.accountId === actor.tenantId);
        assert(this.props.assignedAccountUserId === actor.tenantUserId);
        break;
      default:
        // note assertions and throwing errors are here for brevity,
        // but normally would use something similar to this:
        // https://khalilstemmler.com/articles/enterprise-typescript-nodejs/handling-errors-result-class/
        throw new Error('unknown actor type');
    }

    this.props.text = text;
  }
}

// supporting cast

interface User {
  id: string;
}

interface Context {
  user: User;
}

enum ActorType {
  ADMIN,
  ACCOUNT_ADMIN,
  ACCOUNT_USER
}

interface Admin {
  type: ActorType.ADMIN,
  userId: string
}

interface AccountAdmin {
  type: ActorType.ACCOUNT_ADMIN,
  tenantId: string,
  userId: string
}

interface AccountUser {
  type: ActorType.ACCOUNT_USER,
  tenantUserId: string,
  tenantId: string,
  userId: string
}

async function anAdmin({ userId }: { userId: string }): Promise<Admin> {
  // gets an admin
}

async function anAccountAdmin({ accountId, userId }: { accountId: string, userId: string }): Promise<AccountAdmin> {
  // gets an account admin
}

async function anAccountUser({ accountId, userId }: { accountId: string, userId: string }): Promise<AccountUser> {
  // gets an account user
}

async function hasPermission(permission: string) {
  // checks permissions in cache or calls to external service
}

type Actor = Admin | AccountAdmin | AccountUser;

interface TaskRepository {
  findOne({ taskId }: { taskId: string }): Promise<TaskModel>;
  save(task: TaskModel): Promise<TaskModel>;
}

person Jordan    schedule 01.12.2019    source источник
comment
Сделайте свой вопрос более кратким, возможно, ответ был получен ранее.   -  person xiemeilong    schedule 04.12.2019


Ответы (1)


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

class Permission {
    name: String;
}

class Role {
    name: String;
    permissions: Permission[];
}

class User {
    id: String;
    role: Role;
}

Таким образом, вы можете изменить то, какие разрешения на самом деле имеют роли.

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

class PermissionService {
    userHasRightToUpdateText = (task : Task, user: User) => {
        return (user.role.permissions.some(p => p.name == 'task:updateText:all')
            || (user.role.permissions.some(p => p.name == 'task:updateText:account') && task.accountId == user.id));
            //or whatever you auth logic is
    }
}

class TaskApplicationService {
    constructor(private taskRepository: TaskRepository,
        private userRepository: UserRepository,
        private permissionService : PermissionService) { }
    async updateText({ taskId, text, accountId, context }: { taskId: string, text: string, accountId?: string, context: Context }) {
      const userId = context.user.id;
      const user = await this.userRepository.findOne({ userId });

      const task = await this.taskRepository.findOne({ taskId });

      if (this.permissionService.userHasRightToUpdateText(task, user)) {
        task.updateText({ text });
        await this.taskRepository.save(task);
      }
      else {
        throw new Error('not authorized');
      }
      // return TaskMapper.toDto(task);
    }
  }

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

person Bohdan Stupak    schedule 10.12.2019
comment
Я думал об использовании политик таким образом, и это действительно решает мои проблемы с транзакциями и версиями, хотя мне было бы интересно узнать, как .NET делает такие вещи (docs.microsoft.com/en-us/aspnet/core /security/authorization/) и может ли он по-прежнему предлагать такие же гарантии. Извлечение логики в политику для меня является легкой частью. Трудная часть — определить, с каким типом пользователя я имею дело. В моем примере у меня есть три типа пользователей - пользователь-администратор, который может обновлять текст в любой задаче... - person Jordan; 11.12.2019
comment
администратор учетной записи, который может обновлять текст только для задач под своей учетной записью, а затем пользователь учетной записи, который может обновлять только задачи в своей учетной записи и для которых они были назначены. В моей модели, и я не уверен, что это правильный подход, сущность пользователя учетной записи (а не пользователь) — это та, которая будет назначена задаче. Возможно, мне следует использовать userId для назначения, но тогда, если я удалю этого пользователя из роли пользователя учетной записи (но они все еще находятся в других ролях), как я узнаю, с какими объектами они связаны в качестве пользователя учетной записи / что мне нужно почистить? - person Jordan; 11.12.2019
comment
Для ясности - назначение в моем текущем подходе будет task.accountUserId = accountUser.id. - person Jordan; 11.12.2019
comment
идея моего примера заключается в том, что вам не нужно выяснять, с каким пользователем вы имеете дело. В PermissionsService достаточно иметь возможность определить, какое разрешение имеет пользователь, и проверить наличие дополнительных ограничений, например, принадлежит ли задача данному пользователю. Роль пользователя в этом примере — это просто контейнер для разрешений. - person Bohdan Stupak; 11.12.2019