Skip to content

Commit b3d23ba

Browse files
committed
feat: refactor access control implementation and update resource paths in controllers
1 parent b3c8734 commit b3d23ba

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1043
-276
lines changed

apps/api/src/_common/guards/ac.guard.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1-
import { CanActivate, ExecutionContext, Inject, Injectable, Type } from '@nestjs/common';
1+
import { CanActivate, ExecutionContext, Inject, Injectable, Logger, Type } from '@nestjs/common';
22
import { Reflector } from '@nestjs/core';
33
import { ACGuard as AcGuardInternal, RolesBuilder } from 'nest-access-control';
44
import { META_UNPROTECTED } from '~/_common/decorators/public.decorator';
5+
import { AC_ADMIN_ROLE, AC_GUEST_ROLE } from '../types/ac-types';
56

67
export function AcGuard(): Type<CanActivate> {
78
@Injectable()
89
class CustomAcGuard<User extends any = any> extends AcGuardInternal<User> implements CanActivate {
910
@Inject(Reflector)
1011
public readonly __reflector!: Reflector;
1112

13+
protected logger: Logger = new Logger(CustomAcGuard.name);
14+
1215
public constructor(reflector: Reflector, roleBuilder: RolesBuilder) {
1316
super(reflector, roleBuilder);
1417
}
@@ -23,31 +26,37 @@ export function AcGuard(): Type<CanActivate> {
2326
public async canActivate(context: ExecutionContext): Promise<boolean> {
2427
const roleOrRoles = await this.getUserRoles(context)
2528
const roles = Array.isArray(roleOrRoles) ? roleOrRoles : [roleOrRoles]
26-
console.log('canActivate', roles)
27-
if (roles.includes('admin')) {
29+
//console.log('canActivate', roles)
30+
if (roles.includes(AC_ADMIN_ROLE)) {
2831
return true;
2932
}
3033

31-
return this.isUnProtected(context) || super.canActivate(context);
34+
const result = this.isUnProtected(context) || await super.canActivate(context);
35+
36+
const path = context.switchToHttp().getRequest().route.path;
37+
const method = context.switchToHttp().getRequest().method;
38+
this.logger.verbose(`User wants to access <${method}::${path}>. Roles: [${roles.join(', ')}] -> [${result ? 'is allowed' : 'is not allowed'}]`);
39+
40+
return result;
3241
}
3342

3443
public async getUser(context: ExecutionContext): Promise<User> {
35-
console.log('isUnProtected', this.isUnProtected(context))
44+
//console.log('isUnProtected', this.isUnProtected(context))
3645
if (this.isUnProtected(context)) {
3746
return {} as User;
3847
}
3948

40-
console.log('getUser', await super.getUser(context))
49+
//console.log('getUser', await super.getUser(context))
4150
return await super.getUser(context);
4251
}
4352

4453
public async getUserRoles(context: ExecutionContext): Promise<string | string[]> {
4554
const roles = await super.getUserRoles(context);
4655

47-
console.log('getUserRoles', roles)
56+
//console.log('getUserRoles', roles)
4857

4958
if (!roles || roles.length === 0) {
50-
return ['guest'];
59+
return [AC_GUEST_ROLE];
5160
}
5261

5362
return roles;

apps/api/src/app.module.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ import { ExtensionsModule } from './extensions/extensions.module';
2929
import { SentryModule, SentryGlobalFilter } from '@sentry/nestjs/setup';
3030
import { isConsoleEntrypoint } from './_common/functions/is-cli';
3131
import { AccessControlModule, ACGuard, RolesBuilder } from 'nest-access-control';
32-
import { RolesService } from './core/roles/roles.service';
3332
import { AcGuard } from './_common/guards/ac.guard';
33+
import { AclRuntimeService } from './core/roles/acl-runtime.service';
3434

3535
@Module({
3636
imports: [
@@ -125,9 +125,9 @@ import { AcGuard } from './_common/guards/ac.guard';
125125
}),
126126
AccessControlModule.forRootAsync({
127127
imports: [CoreModule],
128-
inject: [RolesService],
129-
useFactory: async (service: RolesService): Promise<RolesBuilder> => {
130-
return await service.getRolesBuilder()
128+
inject: [AclRuntimeService],
129+
useFactory: async (service: AclRuntimeService): Promise<RolesBuilder> => {
130+
return await service.getGuardRolesBuilder()
131131
},
132132
}),
133133
FactorydriveModule.forRootAsync({

apps/api/src/core/agents/agents.controller.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ export class AgentsController extends AbstractController {
110110
*/
111111
@Get()
112112
@UseRoles({
113-
resource: 'core/agents',
113+
resource: '/core/agents',
114114
action: AC_ACTIONS.READ,
115115
possession: AC_DEFAULT_POSSESSION,
116116
})
@@ -161,7 +161,7 @@ export class AgentsController extends AbstractController {
161161
*/
162162
@Get(':_id([0-9a-fA-F]{24})')
163163
@UseRoles({
164-
resource: 'core/agents',
164+
resource: '/core/agents',
165165
action: AC_ACTIONS.READ,
166166
possession: AC_DEFAULT_POSSESSION,
167167
})
@@ -197,7 +197,7 @@ export class AgentsController extends AbstractController {
197197
*/
198198
@Patch(':_id([0-9a-fA-F]{24})')
199199
@UseRoles({
200-
resource: 'core/agents',
200+
resource: '/core/agents',
201201
action: AC_ACTIONS.UPDATE,
202202
possession: AC_DEFAULT_POSSESSION,
203203
})
@@ -228,7 +228,7 @@ export class AgentsController extends AbstractController {
228228
*/
229229
@Delete(':_id([0-9a-fA-F]{24})')
230230
@UseRoles({
231-
resource: 'core/agents',
231+
resource: '/core/agents',
232232
action: AC_ACTIONS.DELETE,
233233
possession: AC_DEFAULT_POSSESSION,
234234
})

apps/api/src/core/auth/auth.controller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ export class AuthController extends AbstractController {
5050
user: {
5151
...omit(user, ['security', 'metadata']),
5252
sseToken: hash('sha256', user.security.secretKey),
53+
access: ac.getGrants(),
5354
},
54-
access: ac.getGrants(),
5555
});
5656
}
5757

apps/api/src/core/cron/cron.controller.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@ import { Controller, Get, Res, HttpStatus, Query } from '@nestjs/common'
33
import { ApiTags } from '@nestjs/swagger'
44
import { CronService } from './cron.service'
55
import { Response } from 'express'
6+
import { UseRoles } from '~/_common/decorators/use-roles.decorator'
7+
import { AC_ACTIONS, AC_DEFAULT_POSSESSION } from '~/_common/types/ac-types'
8+
import { ApiPaginatedDecorator } from '~/_common/decorators/api-paginated.decorator'
9+
import { PickProjectionHelper } from '~/_common/helpers/pick-projection.helper'
10+
import { CronDto } from './_dto/cron.dto'
11+
import { PartialProjectionType } from '~/_common/types/partial-projection.type'
12+
import { ApiReadResponseDecorator } from '~/_common/decorators/api-read-response.decorator'
613

714
/**
815
* Contrôleur Cron - Endpoints pour les tâches planifiées.
@@ -14,6 +21,12 @@ import { Response } from 'express'
1421
export class CronController {
1522
public constructor(private readonly cronService: CronService) { }
1623

24+
protected static readonly projection: PartialProjectionType<CronDto> = {
25+
name: 1,
26+
description: 1,
27+
schedule: 1,
28+
}
29+
1730
/**
1831
* Endpoint pour rechercher et lister les tâches cron configurées.
1932
*
@@ -24,6 +37,12 @@ export class CronController {
2437
* @returns Une liste paginée des tâches cron avec leurs détails d'exécution
2538
*/
2639
@Get()
40+
@UseRoles({
41+
resource: '/core/cron',
42+
action: AC_ACTIONS.READ,
43+
possession: AC_DEFAULT_POSSESSION,
44+
})
45+
@ApiPaginatedDecorator(PickProjectionHelper(CronDto, CronController.projection))
2746
public async search(
2847
@Query('search') search: string,
2948
@Query('page') page: number,
@@ -43,6 +62,12 @@ export class CronController {
4362
}
4463

4564
@Get(':name')
65+
@UseRoles({
66+
resource: '/core/cron',
67+
action: AC_ACTIONS.READ,
68+
possession: AC_DEFAULT_POSSESSION,
69+
})
70+
@ApiReadResponseDecorator(CronDto)
4671
public async read(
4772
@Res() res: Response,
4873
): Promise<Response> {

apps/api/src/core/jobs/jobs.controller.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import { ObjectIdValidationPipe } from '~/_common/pipes/object-id-validation.pip
1616
import { PartialProjectionType } from '~/_common/types/partial-projection.type';
1717
import { JobsDto } from './_dto/jobs.dto';
1818
import { JobsService } from './jobs.service';
19+
import { UseRoles } from '~/_common/decorators/use-roles.decorator';
20+
import { AC_ACTIONS, AC_DEFAULT_POSSESSION } from '~/_common/types/ac-types';
1921

2022
@ApiTags('core/jobs')
2123
@Controller('jobs')
@@ -35,6 +37,11 @@ export class JobsController extends AbstractController {
3537
}
3638

3739
@Get()
40+
@UseRoles({
41+
resource: '/core/jobs',
42+
action: AC_ACTIONS.READ,
43+
possession: AC_DEFAULT_POSSESSION,
44+
})
3845
@ApiPaginatedDecorator(PickProjectionHelper(JobsDto, JobsController.projection))
3946
public async search(
4047
@Res() res: Response,
@@ -58,6 +65,11 @@ export class JobsController extends AbstractController {
5865
}
5966

6067
@Get(':_id([0-9a-fA-F]{24})')
68+
@UseRoles({
69+
resource: '/core/jobs',
70+
action: AC_ACTIONS.READ,
71+
possession: AC_DEFAULT_POSSESSION,
72+
})
6173
@ApiParam({ name: '_id', type: String })
6274
@ApiReadResponseDecorator(JobsDto)
6375
public async read(
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { Injectable, Logger } from '@nestjs/common'
2+
import { RolesBuilder } from 'nest-access-control'
3+
import { RolesService } from './roles.service'
4+
5+
@Injectable()
6+
export class AclRuntimeService {
7+
private readonly logger = new Logger(AclRuntimeService.name)
8+
private rolesBuilder: RolesBuilder | null = null
9+
private refreshPromise: Promise<RolesBuilder> | null = null
10+
private readonly guardRolesBuilder: RolesBuilder
11+
12+
public constructor(
13+
private readonly rolesService: RolesService,
14+
) {
15+
this.guardRolesBuilder = new Proxy({} as RolesBuilder, {
16+
get: (_target, property, _receiver) => {
17+
if (!this.rolesBuilder) {
18+
throw new Error('ACL roles builder is not initialized yet')
19+
}
20+
21+
const value = Reflect.get(this.rolesBuilder as unknown as object, property)
22+
return typeof value === 'function' ? value.bind(this.rolesBuilder) : value
23+
},
24+
})
25+
}
26+
27+
public async getGuardRolesBuilder(): Promise<RolesBuilder> {
28+
await this.getRolesBuilder()
29+
return this.guardRolesBuilder
30+
}
31+
32+
public async getRolesBuilder(): Promise<RolesBuilder> {
33+
if (this.rolesBuilder) {
34+
return this.rolesBuilder
35+
}
36+
37+
return this.refresh()
38+
}
39+
40+
public async refresh(): Promise<RolesBuilder> {
41+
if (this.refreshPromise) {
42+
return this.refreshPromise
43+
}
44+
45+
this.refreshPromise = this.rolesService.getRolesBuilder()
46+
.then((rolesBuilder) => {
47+
this.rolesBuilder = rolesBuilder
48+
this.logger.log('ACL reloaded from roles collection')
49+
return rolesBuilder
50+
})
51+
.finally(() => {
52+
this.refreshPromise = null
53+
})
54+
55+
return this.refreshPromise
56+
}
57+
}

apps/api/src/core/roles/roles.controller.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { ApiDeletedResponseDecorator } from "~/_common/decorators/api-deleted-re
1717
import { UseRoles } from "~/_common/decorators/use-roles.decorator"
1818
import { AC_ACTIONS, AC_ADMIN_ROLE, AC_DEFAULT_POSSESSION, AC_GUEST_ROLE } from "~/_common/types/ac-types"
1919
import { Roles } from "./_schemas/roles.schema"
20+
import { AclRuntimeService } from "./acl-runtime.service"
2021

2122
@ApiTags('core/roles')
2223
@Controller('roles')
@@ -32,13 +33,16 @@ export class RolesController extends AbstractController {
3233
description: 1,
3334
};
3435

35-
public constructor(private readonly _service: RolesService) {
36+
public constructor(
37+
private readonly _service: RolesService,
38+
private readonly _aclRuntimeService: AclRuntimeService,
39+
) {
3640
super()
3741
}
3842

3943
@Get('list')
4044
@UseRoles({
41-
resource: 'core/roles',
45+
resource: '/core/roles',
4246
action: AC_ACTIONS.READ,
4347
possession: AC_DEFAULT_POSSESSION,
4448
})
@@ -68,7 +72,7 @@ export class RolesController extends AbstractController {
6872

6973
@Get('resources')
7074
@UseRoles({
71-
resource: 'core/roles',
75+
resource: '/core/roles',
7276
action: AC_ACTIONS.READ,
7377
possession: AC_DEFAULT_POSSESSION,
7478
})
@@ -85,7 +89,7 @@ export class RolesController extends AbstractController {
8589

8690
@Get()
8791
@UseRoles({
88-
resource: 'core/roles',
92+
resource: '/core/roles',
8993
action: AC_ACTIONS.READ,
9094
possession: AC_DEFAULT_POSSESSION,
9195
})
@@ -125,13 +129,14 @@ export class RolesController extends AbstractController {
125129

126130
@Post()
127131
@UseRoles({
128-
resource: 'core/roles',
132+
resource: '/core/roles',
129133
action: AC_ACTIONS.CREATE,
130134
possession: AC_DEFAULT_POSSESSION,
131135
})
132136
@ApiCreateDecorator(RolesCreateDto, RolesDto)
133137
public async create(@Res() res: Response, @Body() body: RolesCreateDto): Promise<Response> {
134138
const data = await this._service.create(body)
139+
await this._aclRuntimeService.refresh()
135140
return res.status(HttpStatus.CREATED).json({
136141
statusCode: HttpStatus.CREATED,
137142
data,
@@ -140,7 +145,7 @@ export class RolesController extends AbstractController {
140145

141146
@Get(':_id([0-9a-fA-F]{24})')
142147
@UseRoles({
143-
resource: 'core/roles',
148+
resource: '/core/roles',
144149
action: AC_ACTIONS.READ,
145150
possession: AC_DEFAULT_POSSESSION,
146151
})
@@ -159,7 +164,7 @@ export class RolesController extends AbstractController {
159164

160165
@Patch(':_id([0-9a-fA-F]{24})')
161166
@UseRoles({
162-
resource: 'core/roles',
167+
resource: '/core/roles',
163168
action: AC_ACTIONS.UPDATE,
164169
possession: AC_DEFAULT_POSSESSION,
165170
})
@@ -171,6 +176,7 @@ export class RolesController extends AbstractController {
171176
@Res() res: Response,
172177
): Promise<Response> {
173178
const data = await this._service.update(_id, body)
179+
await this._aclRuntimeService.refresh()
174180
return res.status(HttpStatus.OK).json({
175181
statusCode: HttpStatus.OK,
176182
data,
@@ -179,7 +185,7 @@ export class RolesController extends AbstractController {
179185

180186
@Delete(':_id([0-9a-fA-F]{24})')
181187
@UseRoles({
182-
resource: 'core/roles',
188+
resource: '/core/roles',
183189
action: AC_ACTIONS.DELETE,
184190
possession: AC_DEFAULT_POSSESSION,
185191
})
@@ -190,6 +196,7 @@ export class RolesController extends AbstractController {
190196
@Res() res: Response,
191197
): Promise<Response> {
192198
const data = await this._service.delete(_id)
199+
await this._aclRuntimeService.refresh()
193200
return res.status(HttpStatus.OK).json({
194201
statusCode: HttpStatus.OK,
195202
data,

apps/api/src/core/roles/roles.module.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { MongooseModule } from '@nestjs/mongoose'
44
import { Roles, RolesSchema } from './_schemas/roles.schema'
55
import { RolesService } from './roles.service'
66
import { RolesController } from './roles.controller'
7+
import { AclRuntimeService } from './acl-runtime.service'
78

89
@Module({
910
imports: [
@@ -15,8 +16,8 @@ import { RolesController } from './roles.controller'
1516
},
1617
]),
1718
],
18-
providers: [RolesService],
19+
providers: [RolesService, AclRuntimeService],
1920
controllers: [RolesController],
20-
exports: [RolesService],
21+
exports: [RolesService, AclRuntimeService],
2122
})
2223
export class RolesModule { }

0 commit comments

Comments
 (0)