Skip to content

Commit

Permalink
Merge pull request #148 from conceptadev/feature/access-control-passw…
Browse files Browse the repository at this point in the history
…ord-enforcement

feat: access control refactor for user password enforement
  • Loading branch information
MrMaz authored Feb 5, 2024
2 parents 3c2c692 + a0a7166 commit 42ec8d3
Show file tree
Hide file tree
Showing 87 changed files with 1,521 additions and 1,374 deletions.
164 changes: 75 additions & 89 deletions packages/nestjs-access-control/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,103 +147,89 @@ import { acRules } from './app.acl';
export class AppModule {}
```

### Implement on your controller (nestjsx CRUD module with Passport guard example)
### Implement on your controller (Passport guard example)

```typescript
import { Controller, UseGuards } from '@nestjs/common';
import { Crud, CrudController } from '@nestjsx/crud';
import { User } from './user.entity';
import { UserService } from './user.service';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { AuthGuard } from '@nestjs/passport';
import { AppResource } from '../../app.acl';
import { ApiTags } from '@nestjs/swagger';
import { Controller, Body, Query, Param } from '@nestjs/common';
import {
AccessControlCreateMany,
AccessControlCreateOne,
AccessControlDeleteOne,
AccessControlGuard,
AccessControlReadMany,
AccessControlReadOne,
AccessControlRecoverOne,
AccessControlUpdateOne,
UseAccessControl,
} from '@concepta/nestjs-access-control';
import { UserDto, CreateUserDto, UpdateUserDto } from './dto';
import { UserAccessControlFilterService } from './user-access-control-filter.service';

@ApiTags(AppResource.User)
@ApiBearerAuth()
@Crud({
model: {
type: UserDto,
},
dto: {
create: CreateUserDto,
update: UpdateUserDto,
},
routes: {
only: [
'getManyBase',
'getOneBase',
'createOneBase',
'updateOneBase',
'deleteOneBase',
],
getManyBase: {
decorators: [
ApiOperation({
operationId: 'user_getMany',
}),
AccessControlReadMany(AppResource.UserList),
],
},
getOneBase: {
decorators: [
ApiOperation({
operationId: 'user_getOne',
}),
AccessControlReadOne(
AppResource.User,
async (
params: { id: string },
user: User,
service: UserAccessControlFilterService,
): Promise<boolean> => {
return (
params.id === user.id && true === service.userCanRead(id, user)
);
},
),
],
},
createOneBase: {
decorators: [
ApiOperation({
operationId: 'user_createOne',
}),
AccessControlCreateOne(AppResource.User),
],
},
updateOneBase: {
decorators: [
ApiOperation({
operationId: 'user_updateOne',
}),
AccessControlUpdateOne(AppResource.User),
],
},
deleteOneBase: {
decorators: [
ApiOperation({
operationId: 'user_deleteOne',
}),
AccessControlDeleteOne(AppResource.User),
],
},
},
})
@Controller(AppResource.User)
@UseGuards(AuthGuard(), AccessControlGuard)
@UseAccessControl({ service: UserAccessControlFilterService })
export class UserController implements CrudController<User> {
constructor(public service: UserService) {}

import { UserResource } from './user.types';
import { UserCreateDto } from './dto/user-create.dto';
import { UserCreateManyDto } from './dto/user-create-many.dto';
import { UserUpdateDto } from './dto/user-update.dto';

/**
* User controller.
*/
@Controller('user')
@ApiTags('user')
export class UserController {
/**
* Get many
*/
@AccessControlReadMany(UserResource.Many)
async getMany(@Query() query: unknown) {
// ...
}

/**
* Get one
*/
@AccessControlReadOne(UserResource.One)
async getOne(@Param('id') id: string) {
// ...
}

/**
* Create many
*/
@AccessControlCreateMany(UserResource.Many)
async createMany(@Body() userCreateManyDto: UserCreateManyDto) {
// ...
}

/**
* Create one
*/
@AccessControlCreateOne(UserResource.One)
async createOne(@Body() userCreateDto: UserCreateDto) {
// ...
}

/**
* Update one
*/
@AccessControlUpdateOne(UserResource.One)
async updateOne(
@Param('id') userId: string,
@Body() userUpdateDto: UserUpdateDto,
) {
// ...
}

/**
* Delete one
*/
@AccessControlDeleteOne(UserResource.One)
async deleteOne(@Param('id') id: string) {
// ...
}

/**
* Recover one
*/
@AccessControlRecoverOne(UserResource.One)
async recoverOne(@Param('id') id: string) {
// ...
}
}
```
1 change: 1 addition & 0 deletions packages/nestjs-access-control/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
],
"dependencies": {
"@concepta/nestjs-common": "^4.0.0-alpha.37",
"@concepta/ts-common": "^4.0.0-alpha.37",
"@nestjs/common": "^9.0.0",
"@nestjs/config": "^2.2.0",
"@nestjs/core": "^9.0.0",
Expand Down
58 changes: 58 additions & 0 deletions packages/nestjs-access-control/src/access-control.context.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { AccessControl } from 'accesscontrol';
import { mock } from 'jest-mock-extended';
import { Controller } from '@nestjs/common';
import { ExecutionContext, HttpArgumentsHost } from '@nestjs/common/interfaces';
import { AccessControlContext } from './access-control.context';
import { AccessControlReadOne } from './decorators/access-control-read-one.decorator';
import { ActionEnum } from './enums/action.enum';
import { PossessionEnum } from './enums/possession.enum';

describe(AccessControlContext.name, () => {
it('should return expected values', () => {
@Controller()
@AccessControlReadOne('a_resource_name')
class TestController {}

const rules = new AccessControl();
rules.grant('role1').readAny('a_resource_name');

const argsHost = mock<HttpArgumentsHost>();
argsHost.getRequest.mockReturnValue({ body: { b1: 'xyz' } });

const context = mock<ExecutionContext>();
context.getClass.mockReturnValue(TestController);
context.switchToHttp.mockReturnValue(argsHost);

const expectedAccessControlContext = new AccessControlContext({
request: {
body: { b1: 'xyz' },
},
user: { id: 1234 },
query: {
possession: PossessionEnum.OWN,
resource: 'resource_create_own',
action: ActionEnum.READ,
},
accessControl: rules,
executionContext: context,
});

expect(expectedAccessControlContext.getRequest()).toEqual({
body: { b1: 'xyz' },
});
expect(expectedAccessControlContext.getRequest('body')).toEqual({
b1: 'xyz',
});
expect(expectedAccessControlContext.getRequest('not-a-real-prop')).toEqual(
undefined,
);
expect(expectedAccessControlContext.getUser()).toEqual({ id: 1234 });
expect(expectedAccessControlContext.getQuery()).toEqual({
possession: PossessionEnum.OWN,
resource: 'resource_create_own',
action: ActionEnum.READ,
});
expect(expectedAccessControlContext.getAccessControl()).toEqual(rules);
expect(expectedAccessControlContext.getExecutionContext()).toEqual(context);
});
});
43 changes: 43 additions & 0 deletions packages/nestjs-access-control/src/access-control.context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { AccessControl, IQueryInfo } from 'accesscontrol';
import { ExecutionContext } from '@nestjs/common';
import { AccessControlContextArgsInterface } from './interfaces/access-control-context-args.interface';
import { AccessControlContextInterface } from './interfaces/access-control-context.interface';

export class AccessControlContext implements AccessControlContextInterface {
constructor(private readonly ctxArgs: AccessControlContextArgsInterface) {}

protected hasProp<K extends string>(
obj: unknown,
key: K,
): obj is Record<K, unknown> {
return (
key !== null && obj !== null && typeof obj === 'object' && key in obj
);
}

protected getProp(obj: unknown, prop: string) {
return this.hasProp(obj, prop) ? obj[prop] : undefined;
}

getRequest(property?: string): unknown {
return property?.length
? this.getProp(this.ctxArgs.request, property)
: this.ctxArgs.request;
}

getUser(): unknown {
return this.ctxArgs.user;
}

getQuery(): IQueryInfo {
return this.ctxArgs.query;
}

getAccessControl(): AccessControl {
return this.ctxArgs.accessControl;
}

getExecutionContext(): ExecutionContext {
return this.ctxArgs.executionContext;
}
}
Loading

0 comments on commit 42ec8d3

Please sign in to comment.