现在的企业会使用一些管理系统来管理人力等资源,而这些管理系统通常都会有所谓的 权限设计 (Permission) 来帮助企业做好权限的控管,以免发生权限过大所造成的风险问题。这里再举一个生活化的例子,我们熟悉的 YouTube 推出了 YouTube Premium 机制,只要每个月付点费用就可以 失去观看广告的资格 享受没有广告的高级体验,这也是权限设计的一种。权限设计有非常多种方法,本篇会介绍一个经典的设计 - 以角色为基础的存取控制(Role-based access control),简称 RBAC。
RBAC 的概念很简单,以企业用的管理系统来说,很常将各个使用者赋予特定的 角色(Role),比如说:管理者、员工等,而每种角色所拥有的权限都会有些不同,比如说:管理者可以删除员工,但员工不得删除员工与管理者,这种以「角色」为基础的权限配置方式就是 RBAC。
通常在设计一套 RBAC 的系统都会依照需求而有所不同,难易度也会不同,我认为可以粗略地归类成两种:
如果权限、角色等配置 皆不会随意改变,则属於此种设计,什麽意思呢?假设今天有一套系统,有管理员、员工这两个角色,他们能做的事情是不会随意变更的,这样的需求就会简单许多。
如果权限、角色等是可以让使用者自行配置的,则属於此种设计,像 AWS 提供的服务就有非常复杂的权限配置,每个角色都可以透过勾选的方式来配置它的权限。
实作的方式会因为需求不同而有所不同,最传统的作法就是设计资料库将使用者、角色、权限等资料做关联,当然也有非常多的套件在处理这方面的配置,而我认为 Casbin 是比较值得学习的。
它是一个专门处理权限设计的函式库,可以用来设计 ACL、RBAC、ABAC 等授权机制。看到这个 Logo 可能会觉得很熟悉,没错,它与 Golang 有很大的关系,但它不限於 Golang,在 Node.js、 PHP、Python 等皆可使用,是近年来非常热门的函式库。
提醒:Casbin 对於初学者来说可能会比较难上手,这里我会尽量用最简单的方式来介绍它!
Casbin 由两部分所组成:
存取控制模型简单来说就是用来定义怎麽做验证的地方,也就是验证规则的制定。在 Casbin 我们会制作一个 model.conf
的设定档,它是基於 PERM 模型 来进行配置,让验证规则只需要用一个设定档就可以解决,那什麽是 PERM 模型呢?他们分别是这四个元素:请求 (Request)、政策 (Policy)、验证器 (Matcher)、效果 (Effect),不过,RBAC 还会多一种叫 角色定义 (Role Definition) 的元素。
定义验证时所需使用的参数与顺序,必须包含:主题/实体 (Subject)、对象/资源 (Object) 以及 操作/方式 (Action)。请求的范例格式如下:
[request_definition]
r = sub, obj, act
上述范例的各参数意义如下:
[request_definition]
:定义请求时需要以此作为开头。r
:变数名称,因为定义了 [request_definition]
,该变数就代表了请求。sub
:代表主题,通常主题可以是使用者、角色等。obj
:代表对象,通常对象可以是资源等。act
:代表操作,通常操作会是针对资源所执行的动作名称。用比较白话文的方式来解释的话,可以说成:
请求(r) 提供了「谁(sub) 想要对 什麽东西(obj) 做 什麽动作(act)」的资讯。
我们将这段话带入 RBAC 的概念来重新解释:
请求(r) 提供了「角色(sub) 想要对 某个资源(obj) 做 特定操作(act)」的资讯。
定义政策模型的骨架,使未来可以依照该骨架来制定政策模型。政策的范例格式如下:
[policy_definition]
p = sub, obj, act, eft
上述范例的各参数意义如下:
[policy_definition]
:定义政策时需要以此作为开头。p
:变数名称,因为定义了 [policy_definition]
,该变数就代表了政策。sub
:代表主题。obj
:代表对象。act
:代表操作。eft
:代表 允许(allow) 或 拒绝(deny),非必要项目,预设值为 allow
。用比较白话文的方式来解释的话,可以说成:
政策(p) 制定了「谁(sub) 可不可以(eft) 对 什麽东西(obj) 做 什麽动作(act)」的规则描述。
我们将这段话带入 RBAC 的概念来重新解释:
政策(p) 制定了「某个角色(sub) 可不可以(eft) 对 某个资源(obj) 做 特定操作(act)」的规则描述。
验证请求带来的资讯是否与政策模型制定的规则吻合,是一个条件叙述式,在执行验证流程时,会将请求与政策模型的值带入进行验证。验证器的范例如下:
[matchers]
m = r.sub == p.sub && r.act == p.act && r.obj == p.obj
上述范例的各参数意义如下:
[matchers]
:定义验证器时需要以此作为开头。m
:变数名称,因为定义了 [matchers]
,该变数就代表了验证器。r
:变数名称,前面已经透过 [request_definition]
将它宣告成请求。p
:变数名称,前面已经透过 [policy_definition]
将它宣告成政策。r.sub
:代表请求的主题。p.sub
:代表政策的主题。r.obj
:代表请求的对象。p.obj
:代表政策的对象。r.act
:代表请求的操作。p.act
:代表政策的操作。以上方范例来说,用比较白话文的方式来解释可以说成:
请求主题(r.sub) 必须与 政策主题(p.sub) 相同、请求操作(r.act) 必须与 政策操作(p.act) 相同以及 请求对象(r.obj) 必须与 政策对象(p.obj) 相同。
针对验证结果再进行一个额外的验证。效果的范例如下:
[policy_effect]
e = some(where (p.eft == allow))
上述范例的各参数意义如下:
[policy_effect]
:定义效果时需要以此作为开头。e
:变数名称,因为定义了 [policy_effect]
,该变数就代表了效果。p.eft
:政策的许可值。allow
:eft
的结果之一。以上方范例来说,用比较白话文的方式来解释可以说成:
在验证结果中,只要有一个政策许可值为
allow
就表示通过。
用来实现角色继承的定义,不是必要的配置项目。下方为角色定义的范例:
[role_definition]
g = _, _
上述范例的各参数意义如下:
[role_definition]
:定义角色定义时需要以此作为开头。g
:变数名称,因为定义了 [role_definition]
,该变数就代表了角色定义。在范例中可以看到 _, _
这样的配置,这个意思是前项的角色将会继承後项角色的权限,可以运用这个方式来绑定角色和资源的关系。後面会针对这块做更完整的实作范例与解说。
政策模型是制定角色与资源存取关系的地方,也就是哪些角色可以对哪些资源做哪些操作的明确定义。在 Casbin 中最简单的实作方法就是制定 policy.csv
档,当然,也可以透过资料库来维护这些定义,本篇将会以 csv
档的方式进行介绍与呈现。
定义模型的方法很简单,还记得前面我们定义政策为 p
并且 p = sub, obj, act
吗?我们只要根据这个骨架进行配置即可,需特别注意的是开头必须是指定的政策变数。下方为一个简单的模型定义:
p, role:staff /todos read
可以看到我们使用政策 p
来定义模型,该模型的 sub
为 role:staff
、obj
为 /todos
、act
为 read
,完全呼应了 sub
为角色、obj
为资源、act
为操作资源的动作。
如果我们有一个新的角色叫 role:manager
,他同时也是 role:staff
的一份子,这要如何实现角色继承呢?前面有提到角色定义可以办到,透过使用 g
作为开头并且将继承的角色放在前项、被继承的角色放在後项:
p, role:staff /todos read
p, role:manager /todos write
g, role:manager role:staff
这样在政策模型上他们就是继承关系了,但还有一个地方需要去调整,就是我们前面提到的验证器,它也必须去调用 g
来为 sub
做匹配:
m = g(r.sub, p.sub) && r.act == p.act && r.obj == p.obj
透过 npm
安装 node-casbin:
$ npm install casbin
安装完後,我们在专案目录下新增 casbin
资料夹并建立 model.conf
与 policy.csv
,下方为 model.conf
的内容:
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub) && keyMatch2(r.obj, p.obj) && (r.act == p.act || p.act == '*')
这里稍微解释一下验证器的规则,会发现有一个 keyMatch2
的函式,它主要是用来做路由资源的配对,是很好用的功能,而 p.act == '*'
则表示拥有所有操作权限,当政策模型制定该角色的 act
为 *
时,无论请求带入的 act
是什麽,只要角色跟资源是配对的就是合法的存取。
注意:更多实用的函式可以参考官方文件。
接下来设定 policy.csv
的内容:
p, role:admin, /todos, *
p, role:admin, /todos/:id, *
p, role:staff, /todos, read
p, role:staff, /todos/:id, read
p, role:manager, /todos, create
p, role:manager, /todos/:id, update
g, role:manager, role:staff
可以看到 role:admin
可以对 /todos
与 /todos/:id
做任意操作、role:staff
只可以对 /todos
与 /todos/:id
进行 read
操作、role:manager
继承了 role:staff
并且多了 create
与 update
两个操作。
由於 node-casbin
并没有提供 Nest Module 让我们使用,所以我们会针对其进行包装,透过 CLI 产生 AuthorizationModule
与 AuthorizationService
:
$ nest generate module common/authorization
$ nest generate service common/authorization
Casbin 无论在哪个平台上都只需要建置一个 enforcer
来套用 model.conf
以及 policy.csv
进而使用它的功能,这种第三方物件非常适合用自订 Provider 的方式进行处理,我们先在 src/common/authorization
下新增一个 constants
资料夹并建立 token.const.ts
来存放 token
:
export const AUTHORIZATION_ENFORCER = 'authorization_enforcer';
我会希望 model.conf
与 policy.csv
的路径可以从模组外部提供,所以我这里先建立一个 interface
来制定输入值。在 src/common/authorization
下新增一个 models
资料夹并建立 option.model.ts
:
export interface RegisterOptions {
modelPath: string;
policyAdapter: any;
global?: boolean;
}
modelPath
为 model.conf
的路径,比较需要注意的是 policyAdapter
,由於 Casbin 是支援资料库来管理政策模型的,所以它 enforcer
的 policy
可以透过资料库的 Adapter 进行串接,当然,也可以直接给它 policy.csv
的路径。
再来我们要将 AuthorizationModule
做成一个 DynamicModule
,并将 enforcer
以及 AuthorizationService
进行汇出:
import { DynamicModule, Module } from '@nestjs/common';
import { newEnforcer } from 'casbin';
import { AuthorizationService } from './authorization.service';
import { AUTHORIZATION_ENFORCER } from './constants/token.const';
import { RegisterOptions } from './models/option.model';
@Module({})
export class AuthorizationModule {
static register(options: RegisterOptions): DynamicModule {
const { modelPath, policyAdapter, global = false } = options;
const providers = [
{
provide: AUTHORIZATION_ENFORCER,
useFactory: async () => {
const enforcer = await newEnforcer(modelPath, policyAdapter);
return enforcer;
},
},
AuthorizationService,
];
return {
global,
providers,
module: AuthorizationModule,
exports: [...providers],
};
}
}
现在我们需要设计一个 enum
来与我们角色的资源操作做配对,在 src/common/authorization
下新增一个 types
资料夹并建立 action.type.ts
:
export enum AuthorizationAction {
CREATE = 'create',
READ = 'read',
UPDATE = 'update',
DELETE = 'delete',
NONE = 'none',
}
AuthorizationService
主要就是做权限的检查以及把 HttpMethod 转换成 AuthorizationAction
,值得注意的是 enforcer
的 enforce
方法带入的参数正对应到 model.conf
的请求:
import { Inject, Injectable } from '@nestjs/common';
import { Enforcer } from 'casbin';
import { AUTHORIZATION_ENFORCER } from './constants/token.const';
import { AuthorizationAction } from './types/action.type';
@Injectable()
export class AuthorizationService {
constructor(
@Inject(AUTHORIZATION_ENFORCER) private readonly enforcer: Enforcer,
) {}
public checkPermission(subject: string, object: string, action: string) {
return this.enforcer.enforce(subject, object, action);
}
public mappingAction(method: string): AuthorizationAction {
switch (method.toUpperCase()) {
case 'GET':
return AuthorizationAction.READ;
case 'POST':
return AuthorizationAction.CREATE;
case 'PATCH':
case 'PUT':
return AuthorizationAction.UPDATE;
case 'DELETE':
return AuthorizationAction.DELETE;
default:
return AuthorizationAction.NONE;
}
}
}
最後,我们将外部可能会使用到的元件统一由 index.ts
做汇出:
export { AuthorizationModule } from './authorization.module';
export { AuthorizationService } from './authorization.service';
export { AUTHORIZATION_ENFORCER } from './constants/token.const';
基本上权限设计跟身分验证是脱离不了关系的,还记得 passport
在身分验证完成後会将相关资料塞入请求物件的 user
属性里吗?我们可以运用这样的方式来取得使用者的角色,进而在 RoleGuard
进行角色权限验证。我们透过 CLI 产生 RoleGuard
:
$ nest generate guard common/guards/role
但这里我们没有实作身分验证的功能,所以透过塞假资料来模拟 passport
验证後的情境:
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { Request } from 'express';
import { Observable } from 'rxjs';
import { AuthorizationService } from '../authorization';
@Injectable()
export class RoleGuard implements CanActivate {
constructor(private readonly authorizationService: AuthorizationService) {}
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request: Request = context.switchToHttp().getRequest();
(request as any).user = { role: 'manager' }; // 塞假资料来实测验证功能
const { user, path, method } = request as any;
const action = this.authorizationService.mappingAction(method);
if (!user) {
throw new UnauthorizedException();
}
return this.authorizationService.checkPermission(
`role:${user.role}`,
path,
action,
);
}
}
我们建立 TodoModule
并实作几个 API 来进行测试:
$ nest generate module features/todo
$ nest generate controller features/todo
$ nest generate service features/todo
在 TodoService
设计一个阵列来存放资料,并提供搜寻、更新与删除的功能:
import { Injectable } from '@nestjs/common';
@Injectable()
export class TodoService {
todos = [
{
id: 1,
title: 'Ironman 13th',
completed: false,
},
{
id: 2,
title: 'Study NestJS',
completed: true,
},
];
findById(id: string) {
return this.todos.find((todo) => todo.id === Number(id));
}
updateById(id: string, data: any) {
const todo = this.findById(id);
return Object.assign(todo, data);
}
removeById(id: string) {
const idx = this.todos.findIndex((todo) => todo.id === Number(id));
this.todos.splice(idx, 1);
}
}
接着设计三个 API,调整 TodoController
:
import {
Body,
Controller,
Delete,
Get,
Param,
Patch,
UseGuards,
} from '@nestjs/common';
import { RoleGuard } from '../../common/guards/role.guard';
import { TodoService } from './todo.service';
@Controller('todos')
export class TodoController {
constructor(private readonly todoService: TodoService) {}
@UseGuards(RoleGuard)
@Get(':id')
getTodo(@Param('id') id: string) {
return this.todoService.findById(id);
}
@UseGuards(RoleGuard)
@Patch(':id')
updateTodo(@Param('id') id: string, @Body() body: any) {
return this.todoService.updateById(id, body);
}
@UseGuards(RoleGuard)
@Delete(':id')
removeTodo(@Param('id') id: string) {
this.todoService.removeById(id);
return this.todoService.todos;
}
}
最後,在 AppModule
使用我们制作的 AuthorizationModule
:
import { Module } from '@nestjs/common';
import { join } from 'path';
import { AuthorizationModule } from './common/authorization/authorization.module';
import { TodoModule } from './features/todo/todo.module';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [
AuthorizationModule.register({
modelPath: join(__dirname, '../casbin/model.conf'),
policyAdapter: join(__dirname, '../casbin/policy.csv'),
global: true,
}),
TodoModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
现在我们的角色为 role:manager
,因为继承了 role:staff
,所以拥有读取的功能:
role:manager
本身拥有更新的功能,所以可以顺利更改资源内容:
但 role:manager
并没有删除资源的功能,故无法顺利删除:
权限设计是非常普遍的功能,绝对是值得深入学习的一环,而权限设计的方法有很多,本篇提及的 RBAC 就是非常经典的设计,相信看过这篇的各位都对它有更进一步的认识了!这里附上今天的懒人包:
node-casbin
没有提供 Nest Module,故需要自行包装。enforcer
物件来引入 model
与 policy
。policy
可以用最简单的 csv
来实作,也可以用 Adapter 的方式与资料库连动。
今天的实作内容主要根据网路资源进行。 Django并没有提供官方的社群登入整合模组,在第三方套件上,...
首先bidask那边少一个release,已经做修正 首先先用list_positions抓取手上的...
资讯安全宣导 tags: IT铁人 何谓资讯安全 随着资讯科技进步,资讯安全的重要程度日渐提升,以杰...
目的 将程序的组成转换成有上下阶级的结构(或称:树状结构),方便使用者不论从哪个节点、叶子使用,都可...
一级函数 (First Class Functions) Everything you can do...