Skip to content

Commit b3c8734

Browse files
committed
feat: update dependencies and implement access control for agents management
1 parent 9890095 commit b3c8734

39 files changed

+1994
-31
lines changed

apps/api/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
"microdiff": "^1.5.0",
6060
"mjml": "^4.15.3",
6161
"mongoose": "^8.9.5",
62+
"nest-access-control": "^3.2.0",
6263
"nest-commander": "^3.13.0",
6364
"nest-winston": "^1.10.0",
6465
"nestjs-request-context": "^3.0.0",
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { applyDecorators, SetMetadata } from '@nestjs/common'
2+
import { UseRoles as AccessControlUseRoles } from 'nest-access-control'
3+
4+
export const META_AC_RULE = 'ac:rule'
5+
6+
export type AcRule = {
7+
resource: string
8+
action: string
9+
possession?: string
10+
}
11+
12+
export const UseRoles = (rule: AcRule) =>
13+
applyDecorators(
14+
AccessControlUseRoles(rule as any),
15+
SetMetadata(META_AC_RULE, rule),
16+
)
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { CanActivate, ExecutionContext, Inject, Injectable, Type } from '@nestjs/common';
2+
import { Reflector } from '@nestjs/core';
3+
import { ACGuard as AcGuardInternal, RolesBuilder } from 'nest-access-control';
4+
import { META_UNPROTECTED } from '~/_common/decorators/public.decorator';
5+
6+
export function AcGuard(): Type<CanActivate> {
7+
@Injectable()
8+
class CustomAcGuard<User extends any = any> extends AcGuardInternal<User> implements CanActivate {
9+
@Inject(Reflector)
10+
public readonly __reflector!: Reflector;
11+
12+
public constructor(reflector: Reflector, roleBuilder: RolesBuilder) {
13+
super(reflector, roleBuilder);
14+
}
15+
16+
protected isUnProtected(context: ExecutionContext): boolean {
17+
return this.__reflector.getAllAndOverride<boolean>(META_UNPROTECTED, [
18+
context.getClass(),
19+
context.getHandler(),
20+
]);
21+
}
22+
23+
public async canActivate(context: ExecutionContext): Promise<boolean> {
24+
const roleOrRoles = await this.getUserRoles(context)
25+
const roles = Array.isArray(roleOrRoles) ? roleOrRoles : [roleOrRoles]
26+
console.log('canActivate', roles)
27+
if (roles.includes('admin')) {
28+
return true;
29+
}
30+
31+
return this.isUnProtected(context) || super.canActivate(context);
32+
}
33+
34+
public async getUser(context: ExecutionContext): Promise<User> {
35+
console.log('isUnProtected', this.isUnProtected(context))
36+
if (this.isUnProtected(context)) {
37+
return {} as User;
38+
}
39+
40+
console.log('getUser', await super.getUser(context))
41+
return await super.getUser(context);
42+
}
43+
44+
public async getUserRoles(context: ExecutionContext): Promise<string | string[]> {
45+
const roles = await super.getUserRoles(context);
46+
47+
console.log('getUserRoles', roles)
48+
49+
if (!roles || roles.length === 0) {
50+
return ['guest'];
51+
}
52+
53+
return roles;
54+
}
55+
}
56+
57+
return CustomAcGuard;
58+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export const AC_ADMIN_ROLE = 'admin'
2+
export const AC_GUEST_ROLE = 'guest'
3+
4+
export enum AC_ACTIONS {
5+
CREATE = 'create',
6+
READ = 'read',
7+
UPDATE = 'update',
8+
DELETE = 'delete',
9+
}
10+
11+
export enum AC_POSSESSIONS {
12+
OWN = 'own',
13+
ANY = 'any',
14+
}
15+
16+
export const AC_DEFAULT_POSSESSION = AC_POSSESSIONS.ANY

apps/api/src/app.module.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ import { HttpModule } from '@nestjs/axios';
2828
import { ExtensionsModule } from './extensions/extensions.module';
2929
import { SentryModule, SentryGlobalFilter } from '@sentry/nestjs/setup';
3030
import { isConsoleEntrypoint } from './_common/functions/is-cli';
31+
import { AccessControlModule, ACGuard, RolesBuilder } from 'nest-access-control';
32+
import { RolesService } from './core/roles/roles.service';
33+
import { AcGuard } from './_common/guards/ac.guard';
3134

3235
@Module({
3336
imports: [
@@ -120,6 +123,13 @@ import { isConsoleEntrypoint } from './_common/functions/is-cli';
120123
blockingConnection: true,
121124
}),
122125
}),
126+
AccessControlModule.forRootAsync({
127+
imports: [CoreModule],
128+
inject: [RolesService],
129+
useFactory: async (service: RolesService): Promise<RolesBuilder> => {
130+
return await service.getRolesBuilder()
131+
},
132+
}),
123133
FactorydriveModule.forRootAsync({
124134
imports: [ConfigModule],
125135
inject: [ConfigService],
@@ -150,6 +160,10 @@ import { isConsoleEntrypoint } from './_common/functions/is-cli';
150160
provide: APP_GUARD,
151161
useClass: AuthGuard('jwt'),
152162
},
163+
{
164+
provide: APP_GUARD,
165+
useClass: AcGuard(),
166+
},
153167
// {
154168
// provide: APP_FILTER,
155169
// useClass: AllExceptionFilter,

apps/api/src/core/agents/_dto/agents.dto.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,18 @@ export class AgentsCreateDto extends CustomFieldsDto {
130130
@ApiProperty()
131131
public baseURL?: string
132132

133+
/**
134+
* Rôles de l'agent.
135+
*
136+
* @type {string[]}
137+
* @optional
138+
* @default []
139+
*/
140+
@IsString({ each: true })
141+
@IsOptional()
142+
@ApiProperty()
143+
public roles?: string[]
144+
133145
/**
134146
* Configuration de sécurité de l'agent.
135147
* Contient les clés, restrictions et paramètres de sécurité.

apps/api/src/core/agents/_schemas/agents.schema.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,19 @@ export class Agents extends AbstractSchema {
165165
})
166166
public security: SecurityPart
167167

168+
/**
169+
* Rôles de l'agent.
170+
* Permet de définir les rôles de l'agent.
171+
*
172+
* @type {string[]}
173+
* @default []
174+
*/
175+
@Prop({
176+
type: [String],
177+
default: [],
178+
})
179+
public roles: string[]
180+
168181
/**
169182
* Champs personnalisés définis par l'utilisateur.
170183
* Permet de stocker des données métier spécifiques sans modifier le schéma.

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import { ObjectIdValidationPipe } from '~/_common/pipes/object-id-validation.pip
1919
import { PartialProjectionType } from '~/_common/types/partial-projection.type'
2020
import { AgentsCreateDto, AgentsDto, AgentsUpdateDto } from '~/core/agents/_dto/agents.dto'
2121
import { AgentsService } from './agents.service'
22+
import { AC_ACTIONS, AC_DEFAULT_POSSESSION } from '~/_common/types/ac-types'
23+
import { UseRoles } from '~/_common/decorators/use-roles.decorator'
2224

2325
/**
2426
* Contrôleur pour la gestion des agents
@@ -79,6 +81,11 @@ export class AgentsController extends AbstractController {
7981
* @throws {BadRequestException} Si les données fournies sont invalides
8082
*/
8183
@Post()
84+
@UseRoles({
85+
resource: 'core/agents',
86+
action: AC_ACTIONS.CREATE,
87+
possession: AC_DEFAULT_POSSESSION,
88+
})
8289
@ApiCreateDecorator(AgentsCreateDto, AgentsDto)
8390
public async create(@Res() res: Response, @Body() body: AgentsCreateDto): Promise<Response> {
8491
const data = await this._service.create(body)
@@ -102,6 +109,11 @@ export class AgentsController extends AbstractController {
102109
* @todo Implémenter la recherche arborescente par parentId
103110
*/
104111
@Get()
112+
@UseRoles({
113+
resource: 'core/agents',
114+
action: AC_ACTIONS.READ,
115+
possession: AC_DEFAULT_POSSESSION,
116+
})
105117
@ApiPaginatedDecorator(PickProjectionHelper(AgentsDto, AgentsController.projection))
106118
public async search(
107119
@Res() res: Response,
@@ -148,6 +160,11 @@ export class AgentsController extends AbstractController {
148160
* @throws {NotFoundException} Si l'agent n'est pas trouvé
149161
*/
150162
@Get(':_id([0-9a-fA-F]{24})')
163+
@UseRoles({
164+
resource: 'core/agents',
165+
action: AC_ACTIONS.READ,
166+
possession: AC_DEFAULT_POSSESSION,
167+
})
151168
@ApiParam({ name: '_id', type: String })
152169
@ApiReadResponseDecorator(AgentsDto)
153170
public async read(
@@ -179,6 +196,11 @@ export class AgentsController extends AbstractController {
179196
* @throws {BadRequestException} Si les données fournies sont invalides
180197
*/
181198
@Patch(':_id([0-9a-fA-F]{24})')
199+
@UseRoles({
200+
resource: 'core/agents',
201+
action: AC_ACTIONS.UPDATE,
202+
possession: AC_DEFAULT_POSSESSION,
203+
})
182204
@ApiParam({ name: '_id', type: String })
183205
@ApiUpdateDecorator(AgentsUpdateDto, AgentsDto)
184206
public async update(
@@ -205,6 +227,11 @@ export class AgentsController extends AbstractController {
205227
* @throws {NotFoundException} Si l'agent n'est pas trouvé
206228
*/
207229
@Delete(':_id([0-9a-fA-F]{24})')
230+
@UseRoles({
231+
resource: 'core/agents',
232+
action: AC_ACTIONS.DELETE,
233+
possession: AC_DEFAULT_POSSESSION,
234+
})
208235
@ApiParam({ name: '_id', type: String })
209236
@ApiDeletedResponseDecorator(AgentsDto)
210237
public async remove(

apps/api/src/core/auth/_strategies/jwt.strategy.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,18 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
3636
const user = await this.auth.verifyIdentity(payload);
3737

3838
if (!user) return done(new ForbiddenException(), false);
39+
40+
const roles = [...Array.isArray(payload.identity?.roles) ? payload.identity.roles : []]
41+
if (!roles.includes('admin') && payload.identity?._id === '000000000000000000000000') {
42+
roles.push('admin')
43+
}
44+
3945
return done(null, {
4046
$ref: !payload.scopes.includes('api')
4147
? Agents.name
4248
: Keyrings.name,
4349
...payload?.identity,
50+
roles,
4451
});
4552
}
4653
}

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import { Response } from 'express';
99
import { ReqIdentity } from '~/_common/decorators/params/req-identity.decorator';
1010
import { AgentType } from '~/_common/types/agent.type';
1111
import { hash } from 'crypto';
12-
import { omit, pick } from 'radash';
12+
import { omit } from 'radash';
13+
import { RolesService } from '../roles/roles.service';
1314

1415
@Public()
1516
@ApiTags('core/auth')
@@ -18,6 +19,7 @@ export class AuthController extends AbstractController {
1819
constructor(
1920
protected moduleRef: ModuleRef,
2021
private readonly service: AuthService,
22+
private readonly rolesService: RolesService,
2123
) {
2224
super();
2325
}
@@ -40,11 +42,16 @@ export class AuthController extends AbstractController {
4042
this.logger.debug(`Session request for ${identity._id} (${identity.email})`);
4143
const user = await this.service.getSessionData(identity);
4244
this.logger.debug(`Session data delivered for ${identity._id} (${identity.email}) with ${JSON.stringify(user)}`);
45+
46+
const ac = await this.rolesService.getRolesBuilder()
47+
console.log('ac.getGrants()', ac.getGrants())
48+
4349
return res.status(HttpStatus.OK).json({
4450
user: {
4551
...omit(user, ['security', 'metadata']),
4652
sseToken: hash('sha256', user.security.secretKey),
4753
},
54+
access: ac.getGrants(),
4855
});
4956
}
5057

0 commit comments

Comments
 (0)