【问题标题】:DDD how to properly separate permissions and rolesDDD如何正确分离权限和角色
【发布时间】:2020-03-26 11:25:25
【问题描述】:

假设我有一个允许用户创建、分配和编辑任务的应用程序。该应用程序旨在支持多个客户帐户。此应用程序具有各种角色,但我们将重点关注三个及其与特定操作和资源相关的权限(更新任务的文本) - 管理员(有权更新所有帐户的所有任务的文本),帐户管理员(有权更新属于其帐户的任何任务的文本,但他们可以属于多个帐户)和帐户用户(只有在任务分配给他们时才有权更新该任务的文本)。

这个例子有点做作,角色名称有点过于笼统,但请耐心等待。

这里的目标是尝试找到一种巧妙的方法来分离角色和权限,但似乎角色不可避免地与权限相关联(参见下面的代码)。

也许权限应该只是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>;
}

【问题讨论】:

  • 让你的问题更简洁,或许早点得到答案。

标签: typescript security permissions authorization domain-driven-design


【解决方案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);
    }
  }

希望这会有所帮助。

【讨论】:

  • 我正在考虑以这种方式使用策略,它确实解决了我的事务和版本控制问题,尽管我对 .NET 如何做这样的事情感兴趣 (docs.microsoft.com/en-us/aspnet/core/security/authorization/…) 以及它是否仍然可以提供相同的保证。将逻辑提取到策略中对我来说很容易。困难的部分是确定我正在处理的用户类型。在我的示例中,我有三种类型的用户 - 可以更新任何任务文本的管理员用户...
  • 帐户管理员只能更新其帐户下的任务的文本,然后帐户用户只能更新其帐户中的任务并为其分配任务。在我的模型中,我不确定这是否是正确的方法,帐户用户实体(不是用户)是分配给任务的实体。也许我应该使用userId 进行分配,但是如果我从帐户用户角色中删除该用户(但他们仍处于其他角色),我如何知道他们与帐户容量相关的实体用户/我需要清理什么?
  • 为了清楚起见 - 我当前方法中的分配是task.accountUserId = accountUser.id
  • 我的例子背后的想法是你不需要弄清楚你正在与哪个用户打交道。在PermissionsService 中,能够判断用户拥有哪些权限并检查其他约束(例如任务是否属于相关用户)就足够了。示例中的用户角色只是权限的容器。
猜你喜欢
  • 2021-07-09
  • 1970-01-01
  • 2020-10-18
  • 2016-10-01
  • 2017-12-18
  • 1970-01-01
  • 2021-01-11
  • 1970-01-01
  • 2014-09-15
相关资源
最近更新 更多