Skip to content

Commit 19bc13a

Browse files
committed
chore(): initial commit
1 parent 112de50 commit 19bc13a

20 files changed

+515
-0
lines changed

.editorconfig

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
root = true
2+
3+
[*]
4+
indent_style = space
5+
indent_size = 2
6+
charset = utf-8
7+
trim_trailing_whitespace = true
8+
insert_final_newline = true

.gitignore

+9
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,12 @@ typings/
5959

6060
# next.js build output
6161
.next
62+
63+
### VisualStudioCode ###
64+
.vscode/*
65+
!.vscode/settings.json
66+
!.vscode/tasks.json
67+
!.vscode/launch.json
68+
!.vscode/extensions.json
69+
70+
/dist

.prettierrc

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"singleQuote": true
3+
}

README.md

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# nest-authz
2+
3+
基于 node-casbin 实现的 RBAC 权限控制模块。
4+
5+
## 如何使用
6+
7+
```typescript
8+
// app.module.ts
9+
@Module({
10+
imports: [
11+
AuthZModule.register({
12+
model: 'model.conf',
13+
policy: 'policy.csv'
14+
})
15+
],
16+
controllers: [AppController],
17+
providers: [AppService]
18+
})
19+
export class AppModule {}
20+
```
21+
22+
其中 `policy` 也可以为 adapter, 如:
23+
24+
```typescript
25+
import { TypeOrmModule } from '@nestjs/typeorm';
26+
27+
@Module({
28+
imports: [
29+
AuthZModule.register({
30+
model: 'model.conf',
31+
policy: TypeORMAdapter.newAdapter({
32+
name: 'casbin',
33+
type: 'mysql',
34+
host: 'localhost',
35+
port: 3306,
36+
username: 'root',
37+
password: 'password',
38+
database: 'nestdb'
39+
})
40+
}),
41+
],
42+
controllers: [AppController],
43+
providers: [AppService]
44+
})
45+
```
46+
47+
```typescript
48+
// app.controller.ts
49+
import { Controller, Get } from '@nestjs/common';
50+
import { AppService } from './app.service';
51+
import {
52+
AuthZGuard,
53+
AuthZService,
54+
AuthAction,
55+
AuthPossession,
56+
UsePermissions
57+
} from 'nest-authz';
58+
59+
@Controller()
60+
export class AppController {
61+
constructor(
62+
private readonly authzSrv: AuthZService, // 对 enforcer api 的封装
63+
private readonly appService: AppService
64+
) {}
65+
66+
@Get()
67+
getHello(): string {
68+
return this.appService.getHello();
69+
}
70+
71+
// 只有对用户有任意访问权限的用户才可以读取用户列表
72+
@Get('users')
73+
@UseGuards(AuthZGuard)
74+
@UsePermissions({
75+
action: AuthAction.READ,
76+
resource: 'USER', // 此处为演示使用的 magic string, 其值建议使用变量获取
77+
possession: AuthPossession.ANY
78+
})
79+
async findAllUsers() {
80+
// your code
81+
}
82+
83+
@Get(':id/roles')
84+
@UseGuards(AuthZGuard)
85+
@UsePermissions({
86+
action: AuthAction.READ,
87+
resource: 'USER_ROLES',
88+
possession: AuthPossession.OWN_ANY, // 只有对此资源拥有所有权或者任意权限的用户才可以访问
89+
isOwn: (req: any): boolean => {
90+
// 用户可以添加自定义函数 isOwn, 判别用户是否拥有该资源的所有权
91+
return Number(req.user.id) === Number(req.params.id);
92+
}
93+
})
94+
async findUserRoles(@Param('id') id: string): Promise<string[]> {
95+
return this.authzSrv.getRolesForUser(username);
96+
}
97+
}
98+
```
99+
100+
一般来说,认证后的用户数据保存于请求对象的 `user` 属性中。
101+
AuthGuard 使用从请求对象 `user` 属性中获取到的 `username` 属性判别用户权限。

index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './src';

package.json

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
{
2+
"name": "authz",
3+
"version": "0.0.0-placeholder",
4+
"description": "基于 node-casbin 实现的 RBAC 权限控制模块。",
5+
"main": "dist/index.js",
6+
"directories": {
7+
"test": "test"
8+
},
9+
"scripts": {
10+
"build": "rimraf dist && tsc -p tsconfig.json",
11+
"format": "prettier --write \"src/**/*.ts\"",
12+
"lint": "tslint -p tsconfig.json -c tslint.json",
13+
"test": "jest"
14+
},
15+
"keywords": [],
16+
"author": "dreamdevil00",
17+
"license": "MIT",
18+
"dependencies": {
19+
"casbin": "^2.0.1",
20+
"reflect-metadata": "^0.1.12",
21+
"rxjs": "^6.2.2"
22+
},
23+
"devDependencies": {
24+
"@types/jest": "^24.0.8",
25+
"jest": "^23.5.0",
26+
"prettier": "^1.14.2",
27+
"rimraf": "^2.6.3",
28+
"ts-jest": "^23.1.3",
29+
"ts-node": "^7.0.1",
30+
"tslint": "5.11.0",
31+
"typescript": "^2.9.2"
32+
},
33+
"peerDependencies": {
34+
"@nestjs/common": "^5.4.0",
35+
"@nestjs/core": "^5.4.0"
36+
},
37+
"files": [
38+
"dist",
39+
"LICENSE",
40+
"README.md"
41+
],
42+
"jest": {
43+
"testURL": "http://localhost",
44+
"transform": {
45+
"^.+\\.(ts|tsx)$": "ts-jest"
46+
},
47+
"testMatch": [
48+
"**/test/*.+(ts|tsx)"
49+
],
50+
"moduleFileExtensions": [
51+
"ts",
52+
"tsx",
53+
"js",
54+
"jsx",
55+
"json",
56+
"node"
57+
]
58+
}
59+
}

src/authz.constants.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const AUTHZ_MODULE_OPTIONS = 'AUTHZ_MODULE_OPTIONS';
2+
export const AUTHZ_ENFORCER = 'AUTHZ_ENFORCER';
3+
export const PERMISSIONS_METADATA = '__PERMISSIONS__';

src/authz.guard.ts

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import {
2+
Injectable,
3+
CanActivate,
4+
ExecutionContext,
5+
Inject
6+
} from '@nestjs/common';
7+
import { Reflector } from '@nestjs/core';
8+
import { Observable } from 'rxjs';
9+
import { AUTHZ_ENFORCER, PERMISSIONS_METADATA } from './authz.constants';
10+
import * as casbin from 'casbin';
11+
import { Permission } from './interfaces/permission.interface';
12+
import { UnauthorizedException } from '@nestjs/common';
13+
import { AuthPossession } from './types';
14+
15+
@Injectable()
16+
export class AuthZGuard implements CanActivate {
17+
constructor(
18+
private readonly reflector: Reflector,
19+
@Inject(AUTHZ_ENFORCER) public enforcer: casbin.Enforcer
20+
) {}
21+
22+
canActivate(
23+
context: ExecutionContext
24+
): boolean | Promise<boolean> | Observable<boolean> {
25+
const permissions: Permission[] = this.reflector.get<Permission[]>(
26+
PERMISSIONS_METADATA,
27+
context.getHandler()
28+
);
29+
30+
if (!permissions) {
31+
return true;
32+
}
33+
34+
const request = context.switchToHttp().getRequest();
35+
const user = request.user;
36+
if (!user) {
37+
throw new UnauthorizedException();
38+
}
39+
40+
const { username: uname } = user;
41+
42+
const hasPermission = (
43+
username: string,
44+
permission: Permission
45+
): boolean => {
46+
const { possession, resource, action } = permission;
47+
const poss = [];
48+
49+
if (possession === AuthPossession.OWN_ANY) {
50+
poss.push(AuthPossession.ANY, AuthPossession.OWN);
51+
} else {
52+
poss.push(possession);
53+
}
54+
55+
return poss.some(p => {
56+
if (p === AuthPossession.OWN) {
57+
return (permission as any).isOwn(request);
58+
} else {
59+
return this.enforcer.enforce(username, resource, `${action}:${p}`);
60+
}
61+
});
62+
};
63+
64+
const result = permissions.every(permission =>
65+
hasPermission(uname, permission)
66+
);
67+
68+
return result;
69+
}
70+
}

src/authz.module.ts

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { Module, DynamicModule, Global } from '@nestjs/common';
2+
import { AuthZModuleOptions } from './interfaces';
3+
4+
import { AuthZGuard } from './authz.guard';
5+
6+
import { AUTHZ_MODULE_OPTIONS, AUTHZ_ENFORCER } from './authz.constants';
7+
import * as casbin from 'casbin';
8+
import { AuthZService } from './authz.service';
9+
10+
@Global()
11+
@Module({
12+
providers: [],
13+
exports: []
14+
})
15+
export class AuthZModule {
16+
static register(options: AuthZModuleOptions): DynamicModule {
17+
const moduleOptionsProvider = {
18+
provide: AUTHZ_MODULE_OPTIONS,
19+
useValue: options || {}
20+
};
21+
const enforcerProvider = {
22+
provide: AUTHZ_ENFORCER,
23+
useFactory: async () => {
24+
const isFile = typeof options.policy === 'string';
25+
26+
let policyOption;
27+
28+
if (isFile) {
29+
policyOption = options.policy as string;
30+
} else {
31+
policyOption = await options.policy;
32+
}
33+
34+
return await casbin.newEnforcer(options.model, policyOption);
35+
}
36+
};
37+
return {
38+
module: AuthZModule,
39+
providers: [
40+
moduleOptionsProvider,
41+
enforcerProvider,
42+
AuthZGuard,
43+
AuthZService
44+
],
45+
exports: [
46+
moduleOptionsProvider,
47+
enforcerProvider,
48+
AuthZGuard,
49+
AuthZService
50+
]
51+
};
52+
}
53+
}

0 commit comments

Comments
 (0)