Skip to content

Commit

Permalink
create context external tool validation for restricted contexts
Browse files Browse the repository at this point in the history
  • Loading branch information
IgorCapCoder committed Nov 24, 2023
1 parent 54d703c commit 134390f
Show file tree
Hide file tree
Showing 8 changed files with 294 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ describe('ToolContextController (API)', () => {
isOptional: true,
}),
],
restrictToContexts: [ToolContextType.COURSE],
version: 1,
});
const schoolExternalToolEntity: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({
Expand Down Expand Up @@ -173,6 +174,115 @@ describe('ToolContextController (API)', () => {
// expected body is missed
});
});

describe('when external tool has no restrictions ', () => {
const setup = async () => {
const school: SchoolEntity = schoolFactory.buildWithId();
const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({ school }, [
Permission.CONTEXT_TOOL_ADMIN,
]);

const course: Course = courseFactory.buildWithId({ teachers: [teacherUser], school });

const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.buildWithId({
parameters: [],
restrictToContexts: [],
version: 1,
});
const schoolExternalToolEntity: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({
tool: externalToolEntity,
school,
schoolParameters: [],
toolVersion: 1,
});

const postParams: ContextExternalToolPostParams = {
schoolToolId: schoolExternalToolEntity.id,
contextId: course.id,
displayName: course.name,
contextType: ToolContextType.COURSE,
parameters: [],
toolVersion: 1,
};

await em.persistAndFlush([teacherUser, teacherAccount, course, school, schoolExternalToolEntity]);
em.clear();

const loggedInClient: TestApiClient = await testApiClient.login(teacherAccount);

return {
loggedInClient,
postParams,
};
};

it('should create tool', async () => {
const { postParams, loggedInClient } = await setup();

const response = await loggedInClient.post().send(postParams);

expect(response.statusCode).toEqual(HttpStatus.CREATED);
expect(response.body).toEqual<ContextExternalToolResponse>({
id: expect.any(String),
schoolToolId: postParams.schoolToolId,
contextId: postParams.contextId,
displayName: postParams.displayName,
contextType: postParams.contextType,
parameters: [],
toolVersion: postParams.toolVersion,
});
});
});

describe('when external tool restricts to wrong context ', () => {
const setup = async () => {
const school: SchoolEntity = schoolFactory.buildWithId();
const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({ school }, [
Permission.CONTEXT_TOOL_ADMIN,
]);

const course: Course = courseFactory.buildWithId({ teachers: [teacherUser], school });

const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.buildWithId({
parameters: [],
restrictToContexts: [ToolContextType.BOARD_ELEMENT],
version: 1,
});
const schoolExternalToolEntity: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({
tool: externalToolEntity,
school,
schoolParameters: [],
toolVersion: 1,
});

const postParams: ContextExternalToolPostParams = {
schoolToolId: schoolExternalToolEntity.id,
contextId: course.id,
displayName: course.name,
contextType: ToolContextType.COURSE,
parameters: [],
toolVersion: 1,
};

await em.persistAndFlush([teacherUser, teacherAccount, course, school, schoolExternalToolEntity]);
em.clear();

const loggedInClient: TestApiClient = await testApiClient.login(teacherAccount);

return {
loggedInClient,
postParams,
};
};

it('should return forbidden', async () => {
const { postParams, loggedInClient } = await setup();

const response = await loggedInClient.post().send(postParams);

expect(response.statusCode).toEqual(HttpStatus.FORBIDDEN);
});
});
});

describe('[DELETE] tools/context-external-tools/:contextExternalToolId', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,31 @@ import { Test, TestingModule } from '@nestjs/testing';
import { ContextExternalToolRepo } from '@shared/repo';
import {
contextExternalToolFactory,
externalToolFactory,
legacySchoolDoFactory,
schoolExternalToolFactory,
} from '@shared/testing/factory/domainobject';
import { AuthorizationService } from '@modules/authorization';
import {
AuthorizationContext,
AuthorizationContextBuilder,
AuthorizationService,
ForbiddenLoggableException,
} from '@modules/authorization';
import { ObjectId } from '@mikro-orm/mongodb';
import { Permission } from '@shared/domain';
import { ToolContextType } from '../../common/enum';
import { SchoolExternalTool } from '../../school-external-tool/domain';
import { ContextExternalTool, ContextRef } from '../domain';
import { ContextExternalToolService } from './context-external-tool.service';
import { ToolContextTypesList } from '../../external-tool/controller/dto/response/tool-context-types-list';
import { ExternalTool } from '../../external-tool/domain';
import { ExternalToolService } from '../../external-tool/service';
import { SchoolExternalToolService } from '../../school-external-tool/service';

describe('ContextExternalToolService', () => {
let module: TestingModule;
let service: ContextExternalToolService;
let externalToolService: DeepMocked<ExternalToolService>;
let schoolExternalToolService: DeepMocked<SchoolExternalToolService>;

let contextExternalToolRepo: DeepMocked<ContextExternalToolRepo>;

Expand All @@ -32,11 +44,21 @@ describe('ContextExternalToolService', () => {
provide: AuthorizationService,
useValue: createMock<AuthorizationService>(),
},
{
provide: ExternalToolService,
useValue: createMock<ExternalToolService>(),
},
{
provide: SchoolExternalToolService,
useValue: createMock<SchoolExternalToolService>(),
},
],
}).compile();

service = module.get(ContextExternalToolService);
contextExternalToolRepo = module.get(ContextExternalToolRepo);
externalToolService = module.get(ExternalToolService);
schoolExternalToolService = module.get(SchoolExternalToolService);
});

afterAll(async () => {
Expand Down Expand Up @@ -217,4 +239,113 @@ describe('ContextExternalToolService', () => {
});
});
});

describe('checkContextRestrictions', () => {
describe('when contexts are not restricted', () => {
const setup = () => {
const userId = new ObjectId().toHexString();
const context: AuthorizationContext = AuthorizationContextBuilder.write([Permission.CONTEXT_TOOL_ADMIN]);

const externalTool: ExternalTool = externalToolFactory.build({ restrictToContexts: [] });
const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build();
const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build();

schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool);
externalToolService.findById.mockResolvedValueOnce(externalTool);

return {
userId,
context,
contextExternalTool,
schoolExternalTool,
};
};

it('should find SchoolExternalTool', async () => {
const { userId, context, contextExternalTool } = setup();

await service.checkContextRestrictions(contextExternalTool, userId, context);

expect(schoolExternalToolService.findById).toHaveBeenCalledWith(contextExternalTool.schoolToolRef.schoolToolId);
});

it('should find ExternalTool', async () => {
const { userId, context, contextExternalTool, schoolExternalTool } = setup();

await service.checkContextRestrictions(contextExternalTool, userId, context);

expect(externalToolService.findById).toHaveBeenCalledWith(schoolExternalTool.toolId);
});

it('should not throw', async () => {
const { userId, context, contextExternalTool } = setup();

const func = async () => service.checkContextRestrictions(contextExternalTool, userId, context);

await expect(func()).resolves.not.toThrow();
});
});

describe('when context is restricted to correct context type', () => {
const setup = () => {
const userId = new ObjectId().toHexString();
const context: AuthorizationContext = AuthorizationContextBuilder.write([Permission.CONTEXT_TOOL_ADMIN]);

const externalTool: ExternalTool = externalToolFactory.build({ restrictToContexts: [ToolContextType.COURSE] });
const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build();
const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build({
contextRef: { type: ToolContextType.COURSE },
});

schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool);
externalToolService.findById.mockResolvedValueOnce(externalTool);

return {
userId,
context,
contextExternalTool,
};
};

it('should not throw', async () => {
const { userId, context, contextExternalTool } = setup();

const func = async () => service.checkContextRestrictions(contextExternalTool, userId, context);

await expect(func()).resolves.not.toThrow();
});
});

describe('when context is restricted to wrong context type', () => {
const setup = () => {
const userId = new ObjectId().toHexString();
const context: AuthorizationContext = AuthorizationContextBuilder.write([Permission.CONTEXT_TOOL_ADMIN]);

const externalTool: ExternalTool = externalToolFactory.build({
restrictToContexts: [ToolContextType.BOARD_ELEMENT],
});
const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build();
const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build({
contextRef: { type: ToolContextType.COURSE },
});

schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool);
externalToolService.findById.mockResolvedValueOnce(externalTool);

return {
userId,
context,
contextExternalTool,
};
};

it('should throw ForbiddenLoggableException', async () => {
const { userId, context, contextExternalTool } = setup();

const func = async () => service.checkContextRestrictions(contextExternalTool, userId, context);

await expect(func()).rejects.toThrow(new ForbiddenLoggableException(userId, 'ContextExternalTool', context));
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,19 @@ import { EntityId } from '@shared/domain';
import { ContextExternalToolRepo } from '@shared/repo';
import { ContextExternalTool, ContextRef } from '../domain';
import { ContextExternalToolQuery } from '../uc/dto/context-external-tool.types';
import { ToolContextTypesList } from '../../external-tool/controller/dto/response/tool-context-types-list';
import { ToolContextType } from '../../common/enum';
import { SchoolExternalTool } from '../../school-external-tool/domain';
import { ExternalTool } from '../../external-tool/domain';
import { ExternalToolService } from '../../external-tool/service';
import { SchoolExternalToolService } from '../../school-external-tool/service';
import { AuthorizationContext, ForbiddenLoggableException } from '../../../authorization';

@Injectable()
export class ContextExternalToolService {
constructor(private readonly contextExternalToolRepo: ContextExternalToolRepo) {}
constructor(
private readonly contextExternalToolRepo: ContextExternalToolRepo,
private readonly externalToolService: ExternalToolService,
private readonly schoolExternalToolService: SchoolExternalToolService
) {}

public async findContextExternalTools(query: ContextExternalToolQuery): Promise<ContextExternalTool[]> {
const contextExternalTools: ContextExternalTool[] = await this.contextExternalToolRepo.find(query);
Expand Down Expand Up @@ -49,4 +56,22 @@ export class ContextExternalToolService {

return contextExternalTools;
}

public async checkContextRestrictions(
contextExternalTool: ContextExternalTool,
userId: EntityId,
context: AuthorizationContext
): Promise<void> {
const schoolExternalTool: SchoolExternalTool = await this.schoolExternalToolService.findById(
contextExternalTool.schoolToolRef.schoolToolId
);

const externalTool: ExternalTool = await this.externalToolService.findById(schoolExternalTool.toolId);

if (externalTool.restrictToContexts && externalTool.restrictToContexts[0]) {
if (!externalTool.restrictToContexts.includes(contextExternalTool.contextRef.type)) {
throw new ForbiddenLoggableException(userId, 'ContextExternalTool', context);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest';
import { ObjectId } from '@mikro-orm/mongodb';
import {
Action,
AuthorizationContext,
AuthorizationContextBuilder,
AuthorizationService,
ForbiddenLoggableException,
Expand Down Expand Up @@ -96,13 +97,16 @@ describe('ContextExternalToolUc', () => {
},
});

const context: AuthorizationContext = AuthorizationContextBuilder.write([Permission.CONTEXT_TOOL_ADMIN]);

schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool);
contextExternalToolService.saveContextExternalTool.mockResolvedValue(contextExternalTool);

return {
contextExternalTool,
userId,
schoolId,
context,
};
};

Expand All @@ -126,6 +130,18 @@ describe('ContextExternalToolUc', () => {
);
});

it('should check for context restrictions', async () => {
const { contextExternalTool, userId, schoolId, context } = setup();

await uc.createContextExternalTool(userId, schoolId, contextExternalTool);

expect(contextExternalToolService.checkContextRestrictions).toHaveBeenCalledWith(
contextExternalTool,
userId,
context
);
});

it('should call contextExternalToolValidationService', async () => {
const { contextExternalTool, userId, schoolId } = setup();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export class ContextExternalToolUc {

await this.toolPermissionHelper.ensureContextPermissions(userId, contextExternalTool, context);

await this.contextExternalToolService.checkContextRestrictions(contextExternalTool, userId, context);

await this.contextExternalToolValidationService.validate(contextExternalTool);

const createdTool: ContextExternalTool = await this.contextExternalToolService.saveContextExternalTool(
Expand Down
Loading

0 comments on commit 134390f

Please sign in to comment.