基于三段式命令及筛选器的rbac权限控制方案

基于三段式命令及筛选器的rbac权限控制方案

1. 权限命令的表示

1.1 权限命令

采用三段式设计,将命令划分为类型、操作、属性/对象三个层级。相较于采用常量声明,这样的写法可以支持使用通配符进行设置,减少调用时的代码量。

// 常量
File_Add
File_Delete
File_Switch_Page
File_Switch_Step

// 三段式
File::Add
File::Delete
File::Switch::Page
File::Switch::Step

// 通配符(常量无法做到)
File::Switch::* = [
  File::Switch::Page,
  File::Switch::Step
]
File::*::* = [
  File::Add,
  File::Delete,
  File::Switch::Page,
  File::Switch::Step
]

1.2 权限作用域筛选器

形如 属性名/满足条件的属性值 结构,表示允许通过当前权限的必要属性以及对应的属性值。

// 属性名/满足条件的值
creator/user1  仅允许具有creator属性,且值为user1的对象进行操作
operator/user2  仅允许操作者为user2进行操作
color/red,black 仅允许具有color属性,且值为red或者black##的对象进行操作

2. 权限校验模块

该模块主要分为五大功能:权限命令解析,权限作用域筛选器解析,添加权限命令,移除权限命令以及权限校验。

2.1 权限命令解析

权限命令是三段式命令,中间采用 :: 隔开。因此只要按照规范进行字符串分拆即可。

/**
 * 权限命令解析器
 * @param permissionName 权限命令
 */
function _cmdParser(permissionName: string) {
  const [type, action = '*', attr = '*'] = permissionName.split('::');
  return [type, action, attr];
}

由于允许用户传入通配符格式的权限命令,为了方便计算,内部需要把通配符命令转换成具名命令,方便内部进行匹配

/**
 * 预定义的权限命令表
 */
PermissionHandler.PermissionMap: { [type: string]: { [action: string]: { [attr: string]: boolean } } } = {};

/**
 * 判断命令是否合法
 * @param cmd 解析后的命令
 */
function _valid(cmd: string[]) {
  const [type, action, attr] = cmd;
  if (type === '*' || action == '*') {
    return true;
  }
  return !!(
    PermissionHandler.PermissionMap[type] &&
    PermissionHandler.PermissionMap[type][action] &&
    PermissionHandler.PermissionMap[type][action][attr]
  );
}

/**
 * 获取显式权限命令(Type和Action不能为*)
 * @param cmd 权限命令
 * @returns
 */
function _extractExplicitPermission(cmd: readonly string[]) {
  let typeKeys: string[] = [];
  let actionKeys: string[][] = [];
  let explicitPermission: string[] = [];
  if (cmd[0] === '*') {
    typeKeys = Object.keys(PermissionHandler.PermissionMap);
  } else {
    typeKeys = [cmd[0]];
  }

  if (cmd[1] === '*') {
    typeKeys.forEach((typeKey) => {
      actionKeys.push(Object.keys(PermissionHandler.PermissionMap[typeKey]));
    });
  } else {
    typeKeys.forEach((typeKey) => {
      actionKeys.push([cmd[1]]);
    });
  }

  for (let i = 0; i < typeKeys.length; i++) {
    for (let j = 0; j < actionKeys[i].length; j++) {
      if (cmd[2] === '*') {
          const attrs = Object.keys(PermissionHandler.PermissionMap[typeKeys[i]][actionKeys[i][j]]);
          for (let attr of attrs) {
            const newCmd = [typeKeys[i], actionKeys[i][j], attr];
            if (this._valid(newCmd)) {
              explicitPermission.push(newCmd.join('::'));
            }
          }
        } else {
          const newCmd = [typeKeys[i], actionKeys[i][j], cmd[2]];
          if (this._valid(newCmd)) {
            explicitPermission.push(newCmd.join('::'));
          }
        }
    }
  }
  return explicitPermission;
}

2.2 权限作用域筛选器解析

权限作用域筛选器的格式形如 x/y,z,而且输入可能出现 [x/y, x/z],因此需要把相同属性名进行聚合,并且保证满足条件的属性值不重复。为了提高查找时的性能,这里采用了 Map + Set 结构进行存储,保证在查找时满足 O(1) 时间复杂度。

/**
 * 筛选器解析器
 * @param filters 筛选器列表(形如:creator/xxx)
 */
function _filterParser(filters: readonly string[]): Map<string, Set<string>> {
  const filterMap = new Map<string, Set<string>>();
  for (const filter of filters) {
    const [attr, conditions] = filter.split('/');
    const nSet = new Set(conditions.split(','));
    const oSet = filterMap.get(attr);
    if (oSet) {
      filterMap.set(attr, new Set([...oSet, ...nSet]));
    } else {
      filterMap.set(attr, nSet);
    }
  }
  return filterMap;
}

2.3 添加权限命令

这一模块用于批量添加权限命令及其对应的筛选器。主要工作流程如下:

1. 解析作用域筛选器

2. 解析命令,并提取具名权限命令

3. 具名权限命令设置作用域筛选器

/**
 * 权限拦截器存储结构
 */
const interceptorMap = new Map<string, Map<string, Set<string>>>();

/**
 * 添加权限拦截器
 * @param permissions 权限名称
 * @param filter 筛选器
 */
function setInterceptor(permissions: string[], filter: readonly string[]) {
  let explicitCmds: string[] = [];
  // 解析作用域筛选器
  const filters = _filterParser(filter);
  // 解析命令,并提取具名权限命令
  permissions.forEach((permissionName: string) => {
    const cmd = _cmdParser(permissionName);
    if (!_valid(cmd)) {
      return;
    }
    const explicitCmd = _extractExplicitPermission(cmd);
    explicitCmds = explicitCmds.concat(explicitCmd);
  });
  // 具名权限命令设置作用域筛选器
  explicitCmds.forEach((dCmd) => {
    const newFilters = filters;
    if (interceptorMap.has(dCmd)) {
      const old = interceptorMap.get(dCmd);
      old?.forEach((val, key) => {
        if (!newFilters.has(key)) {
          newFilters.set(key, val);
        }
      })
    }
    interceptorMap.set(dCmd, newFilters);
  });
}

2.4 移除权限命令

这一模块用于批量删除权限命令及其对应的筛选器。主要工作流程如下:

1. 解析命令,并提取具名权限命令

2. 移除对应权限命令

/**
 * 移除权限拦截器
 * @param permissions 权限名称
 */
removeInterceptor(permissions: string[]) {
  let explicitCmds: string[] = [];
  permissions.forEach((permissionName: string) => {
    const cmd = _cmdParser(permissionName);
    if (!_valid(cmd)) {
      return;
    }
    const explicitCmd = _extractExplicitPermission(cmd);
    explicitCmds = explicitCmds.concat(explicitCmd);
  });
  explicitCmds.forEach((dCmd) => {
    interceptorMap.delete(dCmd);
  });
}

2.5 权限校验

这个功能是整个模块的灵魂,前面的所有功能都是为其服务的。其主要工作流程可划分为下列模块:

2.5.1 解析命令

const cmd = _cmdParser(permissionCmd);
// 命令不合法,跳过权限校验
if (!_valid(cmd)) {
  return true;
}

2.5.2 获取最高权限对应的作用域筛选器

由于权限命令是三段命令式,用户可能在不同的层级上设置了不同的权限作用域筛选器。因此,需要给各个层级定义筛选器生效的优先级。

本方案的优先级定义是:

三级具名命令 > 三级模糊命令 > 二级具名命令 > 二级模糊命令 > 一级具名命令 > 一级模糊命令

// 以 File::Switch::Page 举例
// 优先级从高到低依次为

File::Switch::Page
File::Switch::*
File::*::*
*::*::*

对应的代码实现为:

/**
 * 获取最高权限的筛选器
 * @param cmd 权限命令
 */
function getHighestAuthorityFilter(cmd: readonly string[]) {
  const realCmd = cmd.concat([]);
  let loop = 3;
  let cmdStr = '';

  while (loop) {
    cmdStr = realCmd.join('::');
    if (interceptorMap.has(cmdStr)) {
      return interceptorMap.get(cmdStr);
    }
    realCmd[--loop] = '*';
  }

  return undefined;
}

2.5.3 权限可通过性判断

conditions 为待判断的对象,通过遍历对象上与作用域筛选器相同的字段,检测对象上的对应值是否在筛选器的可通过条件列表中。如是,返回校验通过,否则,返回校验不通过。

function checkHasPermission(conditions: any[], filters: Map<string, Set<string>>) {
  let hasPermission = true;
  if (filters) {
    filters.forEach((valSet, attr) => {
      // 短路运算(已评为不满足/属性值带有*)
      if (!hasPermission || valSet.has('*')) return;
      // 任意属性均需要满足条件
      if (attr === '*') {
        for (const obj of conditions) {
          for (const attrKey of Object.keys(obj)) {
            if (!valSet.has(obj[attrKey])) {
              hasPermission = false;
              break;
            }
          }
          if (!hasPermission) {
            break;
          }
        }
      } else {
        for (const obj of conditions) {
          if (!valSet.has(obj[attr])) {
            hasPermission = false;
            break;
          }
        }
      }
    });
  }
  return hasPermission;
}

2.5.4 模块代码总览

/**
 * 判断是否具有操作权限
 * @param permissionName 权限名称
 * @param conditions 受影响的对象
 */
function checkPermission(permissionCmd: string, conditions: readonly { [key: string]: any }[]): boolean {
  // 解析命令
  const cmd = _cmdParser(permissionCmd);
  if (!_valid(cmd)) {
    return true;
  }
  // 获取最高权限对应的作用域筛选器
  const filters = getHighestAuthorityFilter(cmd);
  // 权限可通过性判断
  return checkHasPermission(conditions, filters);
}

3. 最佳实践

// 禁用全部操作权限

setInterceptor(['*::*::*'], ['operator/']);
// 'operator/' 表示对于操作者(operator)这个条件,符合的值是空的,意味着没有操作者能够被允许通过这个权限校验

checkPermission('File::Switch::Page', [{ operator: 'xxx' }])
// false
```

```typescript
// 启用全部操作权限

setInterceptor(['*::*::*'], ['operator/*']);
// 'operator/*' 表示对于操作者(operator)这个条件,符合的值是任意的,意味着所有操作者能够被允许通过这个权限校验

checkPermission('File::Switch::Page', [{ operator: 'xxx' }])
// true