Skip to content

Commit

Permalink
feat: 🔥 [EXL-74] support rules list page
Browse files Browse the repository at this point in the history
support rules list page
  • Loading branch information
tal-rofe committed Nov 14, 2022
1 parent 1f0928c commit add4dd9
Show file tree
Hide file tree
Showing 50 changed files with 924 additions and 113 deletions.
7 changes: 7 additions & 0 deletions apps/backend/src/decorators/library.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createParamDecorator, type ExecutionContext } from '@nestjs/common';

export const Library = createParamDecorator((_: undefined, context: ExecutionContext) => {
const request = context.switchToHttp().getRequest();

return request.policyLibrary;
});
29 changes: 29 additions & 0 deletions apps/backend/src/guards/ruleable-policy.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { BadRequestException, Injectable, type CanActivate, type ExecutionContext } from '@nestjs/common';

import { librariesData } from '@/data/libraries-data';

import type { IJwtTokenPayload } from '../interfaces/jwt-token';
import { DBInlinePolicyService } from '../modules/database/inline-policy.service';

@Injectable()
export class RuleablePolicyGuard implements CanActivate {
constructor(private readonly dbInlinePolicyService: DBInlinePolicyService) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const user = request.user as IJwtTokenPayload;
const userId = user.sub;
const inlinePolicyId = request.params.policy_id as string;

const policyLibrary = await this.dbInlinePolicyService.getPolicyLibrary(inlinePolicyId, userId);
const matchingLibraryData = librariesData.find((library) => library.name === policyLibrary)!;

if (Object.keys(matchingLibraryData.rules ?? {}).length === 0) {
throw new BadRequestException();
}

request.policyLibrary = policyLibrary;

return true;
}
}
24 changes: 18 additions & 6 deletions apps/backend/src/modules/database/inline-policy.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { FilesListType } from '@exlint-dashboard/common';
import { Injectable } from '@nestjs/common';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import type { CodeType, PolicyLibrary, Prisma } from '@prisma/client';

import { librariesData } from '@/data/libraries-data';
Expand All @@ -10,6 +10,19 @@ import { PrismaService } from './prisma.service';
export class DBInlinePolicyService {
constructor(private prisma: PrismaService) {}

public async getPolicyLibrary(policyId: string, userId: string) {
const policyRecord = await this.prisma.inlinePolicy.findFirst({
where: { id: policyId, group: { userId } },
select: { library: true },
});

if (!policyRecord) {
throw new UnauthorizedException();
}

return policyRecord.library;
}

public async createInlinePolicy(
groupId: string,
label: string,
Expand All @@ -23,12 +36,12 @@ export class DBInlinePolicyService {
return createdRecord.id;
}

public async doesInlinePolicyBelongUser(inlinePolicyId: string, userId: string) {
const inlinePolicyDB = await this.prisma.inlinePolicy.findFirst({
where: { id: inlinePolicyId, group: { userId } },
public async doesInlinePolicyBelongUser(policyId: string, userId: string) {
const policyRecord = await this.prisma.inlinePolicy.findFirst({
where: { id: policyId, group: { userId } },
});

return inlinePolicyDB !== null;
return policyRecord !== null;
}

public async isLabelAvailable(userId: string, label: string) {
Expand Down Expand Up @@ -143,7 +156,6 @@ export class DBInlinePolicyService {
isFormConfiguration: true,
rules: { select: { id: true, name: true }, take: 10, skip: 10 * (page - 1) },
description: true,
library: true,
createdAt: true,
},
}),
Expand Down
38 changes: 10 additions & 28 deletions apps/backend/src/modules/database/rule.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import { Injectable } from '@nestjs/common';
import type { Prisma } from '@prisma/client';

import { librariesData } from '@/data/libraries-data';

import { PrismaService } from './prisma.service';

Expand All @@ -21,32 +18,17 @@ export class DBRuleService {
await this.prisma.rule.delete({ where: { id: ruleId } });
}

public async getRules(policyId: string) {
const [policyRecord, policyEnabledRulesRecords] = await this.prisma.$transaction([
this.prisma.inlinePolicy.findUniqueOrThrow({
where: { id: policyId },
select: { library: true },
}),
this.prisma.rule.findMany({
where: { policyId },
select: { id: true, configuration: true, name: true },
}),
]);

const libraryData = librariesData.find((library) => library.name === policyRecord.library)!;

const selectedRules = Object.keys(libraryData.rules!).map((ruleName) => {
const ruleData = libraryData.rules![ruleName]!;
const ruleRecord = policyEnabledRulesRecords.find((ruleRecord) => ruleRecord.name === ruleName);

return {
...ruleData,
id: ruleRecord?.id ?? null,
name: ruleName,
configuration: (ruleRecord?.configuration ?? null) as Prisma.JsonArray | null,
};
public getEnabledRules(policyId: string) {
return this.prisma.rule.findMany({
where: { policyId },
select: { id: true, configuration: true, name: true },
});
}

return selectedRules;
public enableRule(policyId: string, name: string) {
return this.prisma.rule.create({
data: { policyId, name },
select: { id: true },
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import {
ApiTags,
ApiUnauthorizedResponse,
} from '@nestjs/swagger';
import { PolicyLibrary } from '@prisma/client';

import { CurrentUserId } from '@/decorators/current-user-id.decorator';
import { BelongingInlinePolicyGuard } from '@/guards/belonging-inline-policy.guard';
import { Library } from '@/decorators/library.decorator';
import { RuleablePolicyGuard } from '@/guards/ruleable-policy.guard';

import Routes from './inline-policies.routes';
import { GetPolicyRulesResponse } from './classes/get-policy-rules.dto';
Expand All @@ -33,20 +35,21 @@ export class GetPolicyRulesController {
description: 'If access token is missing or invalid, or policy does not belong to user',
})
@ApiInternalServerErrorResponse({ description: "If failed to fetch the policy's rules" })
@UseGuards(BelongingInlinePolicyGuard)
@UseGuards(RuleablePolicyGuard)
@Get(Routes.GET_POLICY_RULES)
@HttpCode(HttpStatus.OK)
public async getPolicyRules(
@CurrentUserId() userId: string,
@Param('policy_id') policyId: string,
@Library() policyLibrary: PolicyLibrary,
@Query('p') page?: string,
): Promise<GetPolicyRulesResponse> {
this.logger.log(
`Will try to fetch policy's rules of policy with an ID: "${policyId}" for a user with an Id: "${userId}"`,
);

const policyData = await this.queryBus.execute<GetPolicyRulesContract, GetPolicyRulesResponse>(
new GetPolicyRulesContract(policyId, page),
new GetPolicyRulesContract(policyId, policyLibrary, page),
);

this.logger.log(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { CqrsModule } from '@nestjs/cqrs';

import { BelongingInlinePolicyGuard } from '@/guards/belonging-inline-policy.guard';
import { BelongingGroupGuard } from '@/guards/belonging-group.guard';
import { RuleablePolicyGuard } from '@/guards/ruleable-policy.guard';

import { QueryHandlers } from './queries/handlers';
import { CommandHandlers } from './commands/handlers';
Expand Down Expand Up @@ -43,6 +44,7 @@ import { EditDescriptionController } from './edit-description.controller';
providers: [
BelongingInlinePolicyGuard,
BelongingGroupGuard,
RuleablePolicyGuard,
...QueryHandlers,
...CommandHandlers,
...EventHandlers,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { PolicyLibrary } from '@prisma/client';

export class GetPolicyRulesContract {
constructor(readonly policyId: string, readonly page?: string) {}
constructor(readonly policyId: string, readonly library: PolicyLibrary, readonly page?: string) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export class GetPolicyRulesHandler implements IQueryHandler<GetPolicyRulesContra
const policyRecord = await this.dbInlinePolicyService.getPolicyRules(contract.policyId, page);

const matchingLibraryData = librariesData.find(
(libraryItem) => libraryItem.name === policyRecord.library,
(libraryItem) => libraryItem.name === contract.library,
)!;

const transformedRules = policyRecord.rules.map((ruleItem) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ApiProperty, ApiResponseProperty } from '@nestjs/swagger';
import type { IEnableRuleDto, IEnableRuleResponseData } from '@exlint-dashboard/common';
import { IsString } from 'class-validator';
import type { Rule } from '@prisma/client';

export class EnableRuleDto implements IEnableRuleDto {
@ApiProperty({ type: String, description: 'The new label for a group', example: 'Yazif Group' })
@IsString()
readonly name!: Rule['name'];
}

export class EnableRuleResponse implements IEnableRuleResponseData {
@ApiResponseProperty({
type: String,
example: '62e5362119bea07115434f4a',
})
public id!: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Body, Controller, HttpCode, HttpStatus, Logger, Param, Post, UseGuards } from '@nestjs/common';
import { QueryBus } from '@nestjs/cqrs';
import {
ApiBadRequestResponse,
ApiBearerAuth,
ApiCreatedResponse,
ApiInternalServerErrorResponse,
ApiOperation,
ApiTags,
ApiUnauthorizedResponse,
} from '@nestjs/swagger';
import { PolicyLibrary } from '@prisma/client';

import { CurrentUserId } from '@/decorators/current-user-id.decorator';
import { Library } from '@/decorators/library.decorator';
import { RuleablePolicyGuard } from '@/guards/ruleable-policy.guard';

import Routes from './rules.routes';
import { EnableRuleContract } from './queries/contracts/enable-rule.contract';
import { EnableRuleDto, EnableRuleResponse } from './classes/enable-rule.dto';

@ApiTags('Rules')
@Controller(Routes.CONTROLLER)
export class EnableRuleController {
private readonly logger = new Logger(EnableRuleController.name);

constructor(private readonly queryBus: QueryBus) {}

@ApiOperation({ description: 'Enable a rule for a policy' })
@ApiBearerAuth('access-token')
@ApiCreatedResponse({ description: 'If successfully enabled the rule', type: EnableRuleResponse })
@ApiBadRequestResponse({ description: "If rule name is invalid or policy's library has no rules" })
@ApiUnauthorizedResponse({
description: 'If access token is either missing or invalid, or policy does not belong to user',
})
@ApiInternalServerErrorResponse({ description: 'If failed to enable the rule' })
@UseGuards(RuleablePolicyGuard)
@Post(Routes.ENABLE_RULE)
@HttpCode(HttpStatus.CREATED)
public async enableRule(
@CurrentUserId() userId: string,
@Param('policy_id') policyId: string,
@Body() enableRuleDto: EnableRuleDto,
@Library() policyLibrary: PolicyLibrary,
): Promise<EnableRuleResponse> {
this.logger.log(
`Will try to enable a rule for a policy with an ID: "${policyId}" for a user with an ID: "${userId}"`,
);

const createdRuleId = await this.queryBus.execute<EnableRuleContract, EnableRuleResponse['id']>(
new EnableRuleContract(policyId, enableRuleDto.name, policyLibrary),
);

this.logger.log(
`Successfully enabled a rule with an ID: "${createdRuleId}" for a user with an ID: "${userId}"`,
);

return { id: createdRuleId };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import {
ApiUnauthorizedResponse,
} from '@nestjs/swagger';
import type { IGetRulesResponseData } from '@exlint-dashboard/common';
import { PolicyLibrary } from '@prisma/client';

import { CurrentUserId } from '@/decorators/current-user-id.decorator';
import { BelongingInlinePolicyGuard } from '@/guards/belonging-inline-policy.guard';
import { RuleablePolicyGuard } from '@/guards/ruleable-policy.guard';
import { Library } from '@/decorators/library.decorator';

import { GetRulesResponse } from './classes/get-rules.dto';
import Routes from './rules.routes';
Expand All @@ -34,19 +36,20 @@ export class GetRulesController {
description: 'If access token is missing or invalid, or policy does not belong to user',
})
@ApiInternalServerErrorResponse({ description: 'If failed to fetch the rules' })
@UseGuards(BelongingInlinePolicyGuard)
@UseGuards(RuleablePolicyGuard)
@Get(Routes.GET_RULES)
@HttpCode(HttpStatus.OK)
public async getFormSchema(
@CurrentUserId() userId: string,
@Param('policy_id') policyId: string,
@Library() policyLibrary: PolicyLibrary,
): Promise<GetRulesResponse> {
this.logger.log(
`Will try to fetch rules and related data of policy with an ID: "${policyId}" for a user with an Id: "${userId}"`,
);

const rulesData = await this.queryBus.execute<GetRulesContract, IGetRulesResponseData['rules']>(
new GetRulesContract(policyId),
new GetRulesContract(policyId, policyLibrary),
);

this.logger.log(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { PolicyLibrary } from '@prisma/client';

export class EnableRuleContract {
constructor(
public readonly policyId: string,
public readonly name: string,
public readonly policyLibrary: PolicyLibrary,
) {}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { PolicyLibrary } from '@prisma/client';

export class GetRulesContract {
constructor(public readonly policyId: string) {}
constructor(public readonly policyId: string, public readonly policyLibrary: PolicyLibrary) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs';
import type { IEnableRuleResponseData } from '@exlint-dashboard/common';
import { BadRequestException } from '@nestjs/common';

import { DBRuleService } from '@/modules/database/rule.service';
import { librariesData } from '@/data/libraries-data';

import { EnableRuleContract } from '../contracts/enable-rule.contract';

@QueryHandler(EnableRuleContract)
export class EnableRuleHandler implements IQueryHandler<EnableRuleContract> {
constructor(private readonly dbRuleService: DBRuleService) {}

async execute(contract: EnableRuleContract): Promise<IEnableRuleResponseData['id']> {
const libraryData = librariesData.find((library) => library.name === contract.policyLibrary)!;

if (!libraryData.rules![contract.name]) {
throw new BadRequestException();
}

const createdRuleRecord = await this.dbRuleService.enableRule(contract.policyId, contract.name);

return createdRuleRecord.id;
}
}
Loading

0 comments on commit add4dd9

Please sign in to comment.