Skip to content

Commit

Permalink
Publish v2.0.3
Browse files Browse the repository at this point in the history
  • Loading branch information
Shady Khalifa committed Dec 9, 2021
1 parent 95bbf21 commit 593e333
Show file tree
Hide file tree
Showing 23 changed files with 1,682 additions and 1,472 deletions.
4 changes: 2 additions & 2 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ gulp.task('build', () => {
.pipe(gulp.dest(dist));
});

gulp.task('move', function() {
gulp.src(['node_modules/nest-access-control/**/*']).pipe(gulp.dest('example/node_modules/nest-access-control'));
gulp.task('move', () => {
return gulp.src(['node_modules/nest-access-control/**/*']).pipe(gulp.dest('example/node_modules/nest-access-control'));
});
2 changes: 1 addition & 1 deletion jest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"moduleFileExtensions": ["ts", "tsx", "js", "json"],
"moduleFileExtensions": ["ts", "tsx", "js"],
"transform": {
"^.+\\.tsx?$": "ts-jest"
},
Expand Down
206 changes: 122 additions & 84 deletions lib/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# Nest Access Control


![MIT](https://img.shields.io/cocoapods/l/AFNetworking.svg?style=flat-square)
[![npm version](https://badge.fury.io/js/nest-access-control.svg)](https://badge.fury.io/js/nest-access-control)
![Open Source Love](https://badges.frapsoft.com/os/v1/open-source.svg?v=102)
Expand All @@ -12,39 +11,39 @@

> TL;DR: recently our system was needing to have a Control Panel, so you can control, and monitor every thing from there, and it was really really needing some Role based access control system, so i build this module for that, it is really cool, so i'd love to share it with you, and any PR are more than welcome :heart:
this module is built on top of [onury's](https://github.com/onury) [accesscontrol](https://github.com/onury/accesscontrol) library
This module is built on top of [onury's](https://github.com/onury) [accesscontrol](https://github.com/onury/accesscontrol) library
here is some of it's Core Features

* Chainable, friendly API.
- Chainable, friendly API.
e.g. `ac.can(role).create(resource)`
* Role hierarchical **inheritance**.
* Define grants **at once** (e.g. from database result) or **one by one**.
* Grant/deny permissions by attributes defined by **glob notation** (with nested object support).
* Ability to **filter** data (model) instance by allowed attributes.
* Ability to control access on **own** or **any** resources.
* Ability to **lock** underlying grants model.
* No **silent** errors.
* **Fast**. (Grants are stored in memory, no database queries.)
* Brutally **tested**.
* TypeScript support.
- Role hierarchical **inheritance**.
- Define grants **at once** (e.g. from database result) or **one by one**.
- Grant/deny permissions by attributes defined by **glob notation** (with nested object support).
- Ability to **filter** data (model) instance by allowed attributes.
- Ability to control access on **own** or **any** resources.
- Ability to **lock** underlying grants model.
- No **silent** errors.
- **Fast**. (Grants are stored in memory, no database queries.)
- Brutally **tested**.
- TypeScript support.

#### + What this Module Provide ?
#### What does this Module Provide?

in this module you will have all these features out of the box, but in nest-ish way.
In this module you will have all these features out of the box, but in nest-ish way.

* It's **Decorator-based** so most of the time you will use decorators in your routes.
* Built-in **ACGuard** so you can go and use it directly.
* Access to the underlying **AccessControl** object from everywhere.
- It's **Decorator-based** so most of the time you will use decorators in your routes.
- Built-in **ACGuard** so you can go and use it directly.
- Access to the underlying **AccessControl** object from everywhere.

## Installation

* NPM:
- NPM:

```bash
npm install nest-access-control --save
```

* Yarn:
- Yarn:

```bash
yarn add nest-access-control
Expand All @@ -54,116 +53,155 @@ yarn add nest-access-control

#### Example

> see example folder for the more code;
> See example folder for the more code
We need to build a Video service so users can share there videos with others, but we need some `admins` to control these videos.

1. let's first define our roles:
1. Let's first define our roles:

to build our roles we will need the `RolesBuilder` class, it extends the `AccessControl` class from `accesscontrol` package.
To build our roles we will need the `RolesBuilder` class, it extends the `AccessControl` class from `accesscontrol` package.

```ts
// app.roles.ts
```ts
// app.roles.ts

export enum AppRoles {
USER_CREATE_ANY_VIDEO = 'USER_CREATE_ANY_VIDEO',
ADMIN_UPDATE_OWN_VIDEO = 'ADMIN_UPDATE_OWN_VIDEO',
}
export enum AppRoles {
USER_CREATE_ANY_VIDEO = 'USER_CREATE_ANY_VIDEO',
ADMIN_UPDATE_OWN_VIDEO = 'ADMIN_UPDATE_OWN_VIDEO',
}

export const roles: RolesBuilder = new RolesBuilder();

roles
.grant(AppRoles.USER_CREATE_ANY_VIDEO) // define new or modify existing role. also takes an array.
.createOwn('video') // equivalent to .createOwn('video', ['*'])
.deleteOwn('video')
.readAny('video')
.grant(AppRoles.ADMIN_UPDATE_OWN_VIDEO) // switch to another role without breaking the chain
.extend(AppRoles.USER_CREATE_ANY_VIDEO) // inherit role capabilities. also takes an array
.updateAny('video', ['title']) // explicitly defined attributes
.deleteAny('video');
```
export const roles: RolesBuilder = new RolesBuilder();

roles
.grant(AppRoles.USER_CREATE_ANY_VIDEO) // define new or modify existing role. also takes an array.
.createOwn('video') // equivalent to .createOwn('video', ['*'])
.deleteOwn('video')
.readAny('video')
.grant(AppRoles.ADMIN_UPDATE_OWN_VIDEO) // switch to another role without breaking the chain
.extend(AppRoles.USER_CREATE_ANY_VIDEO) // inherit role capabilities. also takes an array
.updateAny('video', ['title']) // explicitly defined attributes
.deleteAny('video');
```

> Pro Tip :+1: : Keep all roles organized and in one file e,g: `app.roles.ts`

2. next let's use `AccessControlModule` in our Root module:
2. Next let's use `AccessControlModule` in our Root module:

```ts
// app.module.ts
// app.module.ts
import { roles } from './app.roles';
import { roles } from './app.roles';
@Module({
imports: [AccessControlModule.forRoles(roles)],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
@Module({
imports: [AccessControlModule.forRoles(roles)],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
```

Until now everything is fine, but let's make our application,
assume that we have list of video names, user can - _according to our roles_ - `create:own` new video, and `read:any` video, so let's build it:
```
```ts
// app.controller.ts
...
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@UseGuards(AuthGuard, ACGuard)
@UseRoles({
resource: 'video',
action: 'read',
possession: 'any',
})
@Get()
root(@UserRoles() userRoles: any) {
return this.appService.root(userRoles);
}
}
```

until now everything is fine, but let's make our application,
assume that we have list of video names, user can - _according to our roles_ - `create:own` new video, and `read:any` video, so let's build it
### ForRootAsync

Injecting providers for a RoleBuilder Factory (using a database to populate roles)

```ts
// app.controller.ts
...
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@UseGuards(AuthGuard, ACGuard)
@UseRoles({
resource: 'video',
action: 'read',
possession: 'any',
})
@Get()
root(@UserRoles() userRoles: any) {
return this.appService.root(userRoles);
}
@Injectable()
class RoleProvider {

getRoles(): Promise<string[]> {
return Promise.resolve([
'my-custom-role',
]);
}
}

@Module({
providers: [RoleProvider],
exports: [RoleProvider],
})
class RoleModule {

}

@Module({
imports: [
AccessControlModule.forRootAsync({
imports: [TestModule],
inject: [RoleService],
useFactory: async (roleService: RoleService): Promise<RolesBuilder> => {
return new RolesBuilder(await roleService.getRoles());
},
}),
],
})
export class AccessModule {}
```
Notice the use of `imports` in the forRootAsync method. This will allow you to inject exported providers from the imported module. Injecting providers, provided in the same module as the imported AccessControlModule will result in the provider not being found. This is because the module is created before the providers.

so let's discuss what's going on !
So let's discuss what's going on!

First we introduced two new decorators, actually they are three, but let's see what they can do:

* `@UseRoles({ ... })`: this the most used decorator, it define what roles should user have to access this route.
it may take one or more role, but keep in mind that all roles **must** be satisfied.
the structure of the role is really simple, for example, here we define what resources we have, and the **ACGuard\*** - _Damn, it's a good name for a guard :joy:_ - will check for the user roles, then if the user roles have the permissions to access this resource the guard will return `true`, else it will throw a `ForbiddenException`.
for more information about the structure of roles see `roles.interface.ts` file or read the original [documentation](https://onury.io/accesscontrol/) form `accesscontrol` library [here](https://onury.io/accesscontrol/?api=ac#AccessControl~IQueryInfo) .
- `@UseRoles({ ... })`: this the most used decorator, it define what roles should user have to access this route.
It may take one or more role, but keep in mind that all roles **must** be satisfied.
The structure of the role is really simple, for example, here we define what resources we have, and the **ACGuard\*** - _Damn, it's a good name for a guard :joy:_ - will check for the user roles, then if the user roles have the permissions to access this resource the guard will return `true`, else it will throw a `ForbiddenException`.
For more information about the structure of roles see `roles.interface.ts` file or read the original [documentation](https://onury.io/accesscontrol/) form `accesscontrol` library [here](https://onury.io/accesscontrol/?api=ac#AccessControl~IQueryInfo).
> \*note: for those who are asking what ACGuard stands for, it of course stands for Access Control Guard :smile:
- `UserRoles(<prop>)`: if you want to get access to the user roles directly, maybe you want to check it's roles manually instead of `ACGuard` doing it for you, then that decorator is what you are looking for.
the decorator it really simple, it just return the `req.user.roles` value from the `request` object, but wait, what if the user roles isn't exist in `prop: role`, we know that you will ask this question, so that You can pass an optional property key to the decorator to get it from the user object e.g `@UserRoles('permissions')` will return the `req.user.permissions` instead.
* `UserRoles(<prop>)`: if you want to get access to the user roles directly, maybe you want to check it's roles manually instead of `ACGuard` doing it for you, then that decorator is what you are looking for.
The decorator is really simple, it just return the `req.user.roles` value from the `request` object, but wait, what if the user roles doesn't exist in `prop: role`? We knew that you would ask this question, so you can pass an optional property key to the decorator to get it from the user object e.g `@UserRoles('permissions')` will return the `req.user.permissions` instead.

- `@InjectRolesBuilder()`: If you hate the `ACGuard` - _imo it's a good guard_ - and want to build your own Guard instead, you will likely need to access to the underlying `RolesBuilder` Object , then that decorator is for you, it will inject the `Roles` you have defined before, i.e the object passed to the `AccessControlModule.forRoles(roles)`.
* `@InjectRolesBuilder()`: if you hate the `ACGuard` - _imo it's a good guard_ - and want to build your own Guard instead, you will likely need to access to the underlying `RolesBuilder` Object , then that decorator is for you; it will inject the `Roles` you have defined before, i.e the object passed to the `AccessControlModule.forRoles(roles)`.

4. are you still there ? Ok, that's it, you can go and run the application now, but wait, did someone asked for the `AuthGuard` ?
Ok let's discuss the **LIMITATIONS**
4. Are you still there? Ok, that's it, you can go and run the application now, but wait, did someone asked for the `AuthGuard`?
Ok let's discuss the **LIMITATIONS**.

#### Limitations

First of all, this module built with some assumptions

1. the user object will be exist in `req.user`
2. it is up to you to build your own `AuthGuard` that will attach the `user` object to the `req` object, [read more](https://docs.nestjs.com/guards)
3. the `AuthGuard` must be registered before roles guard, in this case it's `ACGuard`, and of course you can combine the `AuthGuard` and `ACGuard` in one guard, and use it everywhere.
1. The user object will exist in `req.user`
2. It is up to you to build your own `AuthGuard` that will attach the `user` object to the `req` object, [read more](https://docs.nestjs.com/guards)
3. The `AuthGuard` must be registered before roles guard, in this case it's `ACGuard`, and of course you can combine the `AuthGuard` and `ACGuard` in one guard, and use it everywhere.

secondly, i don't think these are limitations, since you can easily build your own guard and you don't need the built-in ones anymore.
Secondly, i don't think these are limitations, since you can easily build your own guard and you don't need the built-in ones anymore.

## CHANGELOG

See [CHANGELOG](CHANGELOG.md) for more information.

## Contributing

you are welcome with this project for contributing, just make a PR
You are welcome with this project for contributing, just make a PR.

## Authors

* **Shady Khalifa** - _Initial work_
- **Shady Khalifa** - _Initial work_

See also the list of [contributors](https://github.com/nestjsx/nest-access-control/contributors) who participated in this project.

## License

This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details.
9 changes: 5 additions & 4 deletions lib/access-control.guard.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ACGuard = void 0;
const common_1 = require("@nestjs/common");
const core_1 = require("@nestjs/core");
const inject_roles_builder_decorator_1 = require("./decorators/inject-roles-builder.decorator");
Expand All @@ -29,15 +30,15 @@ let ACGuard = class ACGuard {
const user = await this.getUser(context);
if (!user)
throw new common_1.UnauthorizedException();
return user.roles;
return user['roles'];
}
async canActivate(context) {
const roles = this.reflector.get('roles', context.getHandler());
if (!roles) {
return true;
}
const userRoles = await this.getUserRoles(context);
const hasRoles = roles.every(role => {
const hasRoles = roles.every((role) => {
const queryInfo = role;
queryInfo.role = userRoles;
const permission = this.roleBuilder.permission(queryInfo);
Expand All @@ -47,8 +48,8 @@ let ACGuard = class ACGuard {
}
};
ACGuard = __decorate([
common_1.Injectable(),
__param(1, inject_roles_builder_decorator_1.InjectRolesBuilder()),
(0, common_1.Injectable)(),
__param(1, (0, inject_roles_builder_decorator_1.InjectRolesBuilder)()),
__metadata("design:paramtypes", [core_1.Reflector,
roles_builder_class_1.RolesBuilder])
], ACGuard);
Expand Down
2 changes: 1 addition & 1 deletion lib/access-control.module.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ export declare class AccessControlModule {
/**
* Register a pre-defined roles
* @param {RolesBuilder} roles A list containing the access grant
* @param {ACOptions} options A configurable options
* definitions. See the structure of this object in the examples.
* @param {ACOptions} options A list of configurable options (currently just one)
*/
static forRoles(roles: RolesBuilder, options?: ACOptions): DynamicModule;
static forRootAsync(options: {
Expand Down
9 changes: 5 additions & 4 deletions lib/access-control.module.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
var AccessControlModule_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.AccessControlModule = void 0;
const common_1 = require("@nestjs/common");
const constants_1 = require("@nestjs/common/constants");
const constants_2 = require("./constants");
Expand All @@ -15,8 +16,8 @@ let AccessControlModule = AccessControlModule_1 = class AccessControlModule {
/**
* Register a pre-defined roles
* @param {RolesBuilder} roles A list containing the access grant
* @param {ACOptions} options A configurable options
* definitions. See the structure of this object in the examples.
* @param {ACOptions} options A list of configurable options (currently just one)
*/
static forRoles(roles, options) {
let controllers = [];
Expand Down Expand Up @@ -66,7 +67,7 @@ let AccessControlModule = AccessControlModule_1 = class AccessControlModule {
}
};
AccessControlModule = AccessControlModule_1 = __decorate([
common_1.Global(),
common_1.Module({})
(0, common_1.Global)(),
(0, common_1.Module)({})
], AccessControlModule);
exports.AccessControlModule = AccessControlModule;
1 change: 1 addition & 0 deletions lib/constants.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ROLES_BUILDER_TOKEN = void 0;
exports.ROLES_BUILDER_TOKEN = '__roles_builder__';
Loading

0 comments on commit 593e333

Please sign in to comment.