Published on

RBAC 角色权限管理及其最小实现

Authors

前言

权限管理是一个 Web 服务基础能力中的基础,几乎是所有生产应用绕不开的课题。权限管理的本质是对资源实现访问隔离。“用户在什么状态下对某个资源采取什么操作”。

经典的设计和 RESTful 的设计一脉相承。RESTful 风格以资源为基础,访问资源通过 GET / PUT / POST / DELETE 几种不同的方式。而权限管理也是,用户对应一个或者多个角色,一个角色对某个资源有不定个操作权限(CRUD 或者其他),最核心的 CRUD 操作其实就对应了 POST / GET / PUT / DELETE。

下面我们从架构设计和具体工程实现上,采用较流行的 RBAC(Role Based Access Control)方案,用 TypeScript 实现一个最小化的角色权限管理能力。

架构设计

业务模型

在做架构设计的时候,抽象实体要注意几点:首先需要穷举;其次实体尽量最小化;再次边界清晰。参考 C4 Modeling,我们首先从角色权限管理系统与上下游交互的层面看清楚系统的职责和边界:

RBAC in Application
Application
Controller
Model
View
Logic
RBAC
User
RBAC in Application

深入到角色权限体系本身,它至少应该包含以下实体:

RBAC System ER DiagramUserRolePermissionResourceActionhashasparentcontainscontainswithRBAC System ER Diagram
  • 用户可以有 0 个或者多个角色,用户系统和角色是独立的。有的系统也会设定用户至少有一种角色(譬如 Guest)
  • 角色对应 0 个或者多个权限,在实操中,为了方便定义更复杂的权限组合,通常会让角色具备“继承”的特性,譬如 SuperAdministrator 拥有 Administrator 的所有权限,还可以有额外的权限,这个时候 SuperAdministrator 的 parent 就是 Administrator,鉴权的时候,只要 Administrator 有某个权限,SuperAdministrator 也同样具备
  • 权限由资源和对资源的访问操作组成,这是一一对应的关系。实操中,也有把操作用类似“*”代表全部操作权限的简化设计

物理模型

落到数据库表里的最简设计是 3 个表(不包含用户表、用户角色关联表):

RBAC System TablesRoleintidstringnameintparentIdGrantintroleIdintpermissionIdPermissionintidstringactionstringresourceparentbelongs toforRBAC System Tables

角色表

class Role extends Model {
  declare id;
  declare name;
  declare parentId;
}
Role.init({
  id: {
    type: DataTypes.INTERGER,
    primaryKey: true,
    autoIncrement: true,
  },
  name: {
    type: DataTypes.STRING,
    unique: true,
  },
  parentId: DataTypes.INTERGER,
});
Role.belongsTo(Role, { as: 'parent', foreignKey: 'parentId' });

权限点表

class Permission extends Model {
  declare id;
  declare action;
  declare resource;
  get name() {
    return `${this.action}#${this.resource}`;
  }
}
Permission.init({
  id: {
    type: DataTypes.INTERGER,
    primaryKey: true,
    autoIncrement: true,
  },
  action: DataTypes.STRING,
  resource: DataTypes.STRING,
}, {
  indexes: [
    { fields: ['action', 'resource'], unique: true },
  ],
});

角色权限关联表

class Grant extends Model {
  declare roleId;
  declare permissionId;
  get key() {
    return `${this.roleId}#${this.permissionId}`;
  }
}
Grant.init({
  roleId: DataTypes.INTERGER,
  permissionId: DataTypes.INTERGER,
}, {
  indexes: [
    { fields: ['roleId', 'permissionId'], unique: true },
  ],
});
Grant.belongsTo(Role, { as: 'role', foreignKey: 'roleId' });
Grant.belongsTo(Permission, { as: 'permission', foreignKey: 'permissionId' });

如果是一个 Node.js 后端实现的服务,这就是一个完整的权限管理数据模型的框架了。最小化的物理模型没有体现 Action、Resource 作为独立存储,这是出于复杂度和性能的考量。通常这两者使用字符串就可以表达了,如果要单独作为数据库表,会额外多出两个实体表 + 两个关联表,存储和查询的复杂度都扩大太多,得不偿失。

而角色和权限之间的关联表则是实体表以外最关键的一个,查询某个角色是否具备某个权限,就到这个表查询对应 id 的记录就可以了。赋权和鉴权实质上就是对这个表的增删改查。而角色权限之间的继承关系则由 Role 表的 parentId 字段来实现,为某个角色鉴权的时候,如果这个角色存在父角色,则递归鉴权。

API

对于一个可以闭环的 RBAC 系统,它最基础的 API 是以下几个:

const rbac = new RBAC();
// 增加(删除)角色
const role = rbac.addRole(roleName);
// 增加(删除)权限
const permission = rbac.addPermission(action, resource);
// 授权
rbac.add(role, permission);
// 撤销授权
rbac.revoke(role, permission);
// 鉴权
if (rbac.can(roleName, action, resource)) return true; // 同步版本
const isValid = await rbac.can(roleName, action, resource); // 异步版本
// 如果涉及到落库,以上接口都应该是同上“can”的异步的版本

以上 API 可以最小化地表达一个权限系统,并且完成这个系统所有实例的生命周期管理,以及提供它需要对外提供的两个最核心的能力:(取消)授权和鉴权。

RBAC 最小化实现

如果把存储方案假定为内存存储,所有 API 提供同步版本的前提下,RBAC 系统的最小化实现方案如下。这个方案里弱化了基于关系型数据库的 id,以及基于主键进行关联的概念和操作,以字符拼接为主。

// 资源和动作的分隔符
export const DELIMITER = "_";

// TODO: 所有资源动作的通配符
// export const ALL_ACTIONS_SYMBOL = "*";
/**
 * 是否为有效的角色、资源或者动作名称
 * name 必须非空,并且不能包含 DELIMITER 和空白字符
 * @param name 待检测的角色、资源或者动作名称
 * @returns boolean
 */
const REGEXP_VALID_NAME = new RegExp(`^[^${DELIMITER}\s]+$`);
export const isValidName = (name: string) =>
  !!name && REGEXP_VALID_NAME.test(name);

角色类

import { isValidName } from "./utils";

export default class Role {
  declare name: string; // 角色名称
  declare parent: Role | null; // 父角色,如果没有父角色,则为 null
  /**
   * 初始化角色
   * @param name 角色名称
   * @param parent 父角色
   */
  constructor(name: string, parent: Role | null = null) {
    if (!isValidName(name)) {
      throw new Error(`Invalid role name: ${name}`);
    }
    this.name = name;
    if (parent) {
      this.setParent(parent);
    }
  }
  /**
   * 设置父角色
   * @param parent 父角色
   */
  setParent(parent: Role) {
    this.parent = parent;
  }
}

权限点类

import { DELIMITER, isValidName } from "./utils";

export default class Permission {
  declare resource: string; // 资源对象
  declare action: string; // 操作
  declare name: string; // 权限点名称
  /**
   * 从权限点名称解析出操作和资源
   * @param name 权限点名称
   * @returns 返回操作和资源
   */
  static parse(name: string) {
    if (!name) throw new Error(`Invalid permission name: ${name}`);
    const pos = name.indexOf(DELIMITER);
    if (pos === -1) throw new Error(`Invalid permission name: ${name}`);
    return {
      action: name.substring(0, pos),
      resource: name.substring(pos + 1),
    };
  }
  /**
   * 生成权限点名称
   * @param action 操作
   * @param resource 资源
   * @returns string
   */
  static stringify(action: string, resource: string) {
    if (!isValidName(resource))
      throw new Error(`Invalid resource name: ${resource}`);
    if (!isValidName(action)) throw new Error(`Invalid action name: ${action}`);
    return `${action}${DELIMITER}${resource}`;
  }
  /**
   * 初始化权限点
   * @param action 操作
   * @param resource 资源
   */
  constructor(action: string, resource: string) {
    const name = Permission.stringify(action, resource);
    this.resource = resource;
    this.action = action;
    this.name = name;
  }
}

RBAC 主类

import Role from "./Role";
import Permission from "./Permission";
import { DELIMITER } from "./utils";

export * from "./utils";
export { default as Role } from "./Role";
export { default as Permission } from "./Permission";

export default class RBAC {
  declare roles: Map<string, Role>; // 角色列表
  declare permissions: Map<string, Permission>; // 权限点列表
  declare grants: {
    // 授权列表
    // (`${role.name}${DELIMITER}${permission.name}`: [role.name, permission.action, permission.resource])
    // 保留了角色名称、操作和资源的信息,方便后续落库
    [key: string]: string[];
  };
  /**
   * 初始化 RBAC
   */
  constructor() {
    this.roles = new Map();
    this.permissions = new Map();
    this.grants = {};
  }
  /**
   * 获取 grants 的 key
   * @param role 角色实例
   * @param permission 权限点实例
   * @returns string
   */
  getGrantKey(role: Role, permission: Permission) {
    return `${role.name}${DELIMITER}${permission.name}`;
  }
  /**
   * 获取角色实例
   * @param name 角色名称
   * @returns Role
   */
  getRole(name: string) {
    return this.roles.get(name) || null;
  }
  /**
   * 创建角色
   * @param name 角色名称
   * @param parentName 父亲角色名称
   * @returns Role
   */
  createRole(name: string, parentName?: string) {
    if (this.roles.has(name)) throw new Error(`Role ${name} already exists`);
    const parentRole = this.getRole(parentName);
    const role = new Role(name, parentRole);
    this.roles.set(name, role);
    return role;
  }
  /**
   * 创建权限点
   * @param action 操作
   * @param resource 资源
   * @returns Permission
   */
  createPermission(action: string, resource: string) {
    const key = Permission.stringify(action, resource);
    if (this.permissions.has(key))
      throw new Error(`Permission ${key} already exists`);
    const permission = new Permission(action, resource);
    this.permissions.set(key, permission);
    return permission;
  }
  /**
   * 添加授权 grant
   * @param role 角色实例
   * @param permission 权限点实例
   */
  add(role: Role, permission: Permission) {
    const granKey = this.getGrantKey(role, permission);
    this.grants[granKey] = [role.name, permission.action, permission.resource];
  }
  /**
   * 撤销授权 grant
   * @param role 角色实例
   * @param permission 权限点实例
   */
  revoke(role: Role, permission: Permission) {
    const granKey = this.getGrantKey(role, permission);
    delete this.grants[granKey];
  }
  /**
   * 角色鉴权
   * @param role 角色名称
   * @param action 操作
   * @param resource 资源
   * @returns boolean
   */
  can(role: string, action: string, resource: string): boolean {
    const roleInstance = this.getRole(role);
    const permissionInstance = this.permissions.get(
      Permission.stringify(action, resource)
    );
    if (!roleInstance || !permissionInstance) return false;
    const granKey = this.getGrantKey(roleInstance, permissionInstance);
    if (this.grants[granKey]) return true; // 如果鉴权成功,直接返回 true
    if (roleInstance.parent)
      return this.can(roleInstance.parent.name, action, resource); // 递归检查父角色权限
    return false;
  }
}

demo

import createDebug from "debug";
import RBAC from "../RBAC";

const debug = createDebug("RBAC/demo");
debug("initializing RBAC Roles and Permissions, etc.");
const rbac = new RBAC();
// 创建角色
const guest = rbac.createRole("Guest");
const user = rbac.createRole("User", "Guest");
const developer = rbac.createRole("Developer", "User");
const administrator = rbac.createRole("Administrator", "Developer");
const superAdministrator = rbac.createRole("SuperAdministrator", "Administrator");
// 角色授权
rbac.add(user, rbac.createPermission("read", "repo"));
rbac.add(developer, rbac.createPermission("update", "repo"));
rbac.add(administrator, rbac.createPermission("create", "repo"));
rbac.add(administrator, rbac.createPermission("delete", "repo"));
rbac.add(superAdministrator, rbac.createPermission("update", "$RBAC"));
debug("Roles and Permissions initialized");
// 鉴权
rbac.can("Guest", "read", "repo"); // false
rbac.can("Developer", "read", "repo"); // true // 从 User 继承
rbac.can("Administrator", "update", "$RBAC"); // false
rbac.can("SuperAdministrator", "delete", "repo"); // true // 从 Administrator 继承