Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BC-4709 Create authorisation service #4614

Merged
merged 47 commits into from
Jan 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
7d4b9b0
Add initial authorisation
blazejpass Dec 5, 2023
848142f
Add HttpModule to spec file
blazejpass Dec 5, 2023
62585e3
Add HttpModule to test module
blazejpass Dec 7, 2023
43aa723
Add HttpService to test module
blazejpass Dec 7, 2023
3d0b014
Remove HttpService from test module
blazejpass Dec 8, 2023
9049c24
Add test for added code
blazejpass Dec 8, 2023
7291a87
Merge branch 'main' into BC-4709-create-authorization-service
blazejpass Dec 8, 2023
094be11
Add test for permission endpoint
blazejpass Dec 12, 2023
5a65d5b
Merge branch 'main' into BC-4709-create-authorization-service
blazejpass Dec 13, 2023
193a5d0
Input suggested changes
blazejpass Dec 14, 2023
2148b0e
Change params usage in test file
blazejpass Dec 14, 2023
b97d686
Adjust and add tests for jwt coockie code
blazejpass Dec 14, 2023
9140308
Merge branch 'main' into BC-4709-create-authorization-service
blazejpass Dec 14, 2023
a8b06c1
Improve code
blazejpass Dec 15, 2023
6fd01b8
Merge remote-tracking branch 'origin/BC-4709-create-authorization-ser…
blazejpass Dec 15, 2023
4454250
delete unnecessary import
blazejpass Dec 15, 2023
4a039cf
Merge branch 'main' into BC-4709-create-authorization-service
blazejpass Dec 15, 2023
3effbf5
Adjust and add tests
blazejpass Dec 18, 2023
ee37b50
Merge remote-tracking branch 'origin/BC-4709-create-authorization-ser…
blazejpass Dec 18, 2023
7d86f36
Merge branch 'main' into BC-4709-create-authorization-service
blazejpass Dec 18, 2023
ce90e28
Fix code according to review suggestions
blazejpass Dec 21, 2023
2b60e55
Merge remote-tracking branch 'origin/BC-4709-create-authorization-ser…
blazejpass Dec 21, 2023
56d9fee
Fix code according to review suggestions
blazejpass Dec 28, 2023
aab07ab
Merge branch 'main' into BC-4709-create-authorization-service
blazejpass Dec 28, 2023
621ca4f
Fix ws close error loggable
blazejpass Dec 29, 2023
d8b6cd6
Merge remote-tracking branch 'origin/BC-4709-create-authorization-ser…
blazejpass Dec 29, 2023
f14dce6
Fix api test
blazejpass Dec 29, 2023
96d7e61
Fix repo spec ts after api test changes
blazejpass Dec 29, 2023
3bc32db
Fix tldraw.ws.api.spec.ts tests
blazejpass Dec 29, 2023
df474ce
Fix tldraw.service.spec.ts tests
blazejpass Dec 29, 2023
40e1bd0
Fix test for failed established connection
blazejpass Dec 29, 2023
b05a298
Update element.controller.ts
blazejpass Jan 3, 2024
5a8400d
Improve tests according to comments
blazejpass Jan 3, 2024
12a304c
Merge remote-tracking branch 'origin/BC-4709-create-authorization-ser…
blazejpass Jan 3, 2024
6e951ea
Move extraction of cookies after docName and feature flag check
blazejpass Jan 3, 2024
669554f
Add sentence to 4500 ws close connection
blazejpass Jan 3, 2024
199cd39
Merge branch 'main' into BC-4709-create-authorization-service
blazejpass Jan 3, 2024
522ec38
Merge branch 'main' into BC-4709-create-authorization-service
blazejpass Jan 3, 2024
b7b74dc
Change api-host, add return to try catch
blazejpass Jan 4, 2024
7de9244
Add 404 for drawing authorization proccess
blazejpass Jan 4, 2024
b696577
Fix 404 for drawing authorization proccess
blazejpass Jan 5, 2024
4176da2
Merge branch 'main' into BC-4709-create-authorization-service
blazejpass Jan 8, 2024
f9763e4
Add case of wrong mongoId passed from client
blazejpass Jan 8, 2024
ea099fb
Merge remote-tracking branch 'origin/BC-4709-create-authorization-ser…
blazejpass Jan 8, 2024
d4d9802
Merge branch 'main' into BC-4709-create-authorization-service
blazejpass Jan 9, 2024
935806c
Merge branch 'main' into BC-4709-create-authorization-service
blazejpass Jan 9, 2024
742f95b
Merge branch 'main' into BC-4709-create-authorization-service
blazejpass Jan 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { EntityManager } from '@mikro-orm/mongodb';
import { ServerTestModule } from '@modules/server';
import { INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { BoardExternalReferenceType } from '@shared/domain/domainobject';
import {
TestApiClient,
UserAndAccountTestFactory,
cardNodeFactory,
cleanupCollections,
columnBoardNodeFactory,
columnNodeFactory,
courseFactory,
} from '@shared/testing';
import { drawingElementNodeFactory } from '@shared/testing/factory/boardnode/drawing-element-node.factory';

const baseRouteName = '/elements';
describe('drawing permission check (api)', () => {
let app: INestApplication;
let em: EntityManager;
let testApiClient: TestApiClient;

beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [ServerTestModule],
}).compile();

app = module.createNestApplication();
await app.init();
em = module.get(EntityManager);
testApiClient = new TestApiClient(app, baseRouteName);
});

afterAll(async () => {
await app.close();
});

describe('when user is a valid teacher who is part of course', () => {
const setup = async () => {
await cleanupCollections(em);

const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher();
const course = courseFactory.build({ teachers: [teacherUser] });
await em.persistAndFlush([teacherAccount, teacherUser, course]);

const columnBoardNode = columnBoardNodeFactory.buildWithId({
context: { id: course.id, type: BoardExternalReferenceType.Course },
});

const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode });

const cardNode = cardNodeFactory.buildWithId({ parent: columnNode });

const drawingItemNode = drawingElementNodeFactory.buildWithId({ parent: cardNode });

await em.persistAndFlush([columnBoardNode, columnNode, cardNode, drawingItemNode]);
em.clear();

const loggedInClient = await testApiClient.login(teacherAccount);

return { loggedInClient, teacherUser, columnBoardNode, columnNode, cardNode, drawingItemNode };
};

it('should return status 200', async () => {
const { loggedInClient, drawingItemNode } = await setup();

const response = await loggedInClient.get(`${drawingItemNode.id}/permission`);

expect(response.status).toEqual(200);
});
});

describe('when only teacher is part of course', () => {
const setup = async () => {
await cleanupCollections(em);

const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher();
const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent();

const course = courseFactory.build({ students: [teacherUser] });
await em.persistAndFlush([teacherAccount, teacherUser, course, studentAccount, studentUser]);

const columnBoardNode = columnBoardNodeFactory.buildWithId({
context: { id: course.id, type: BoardExternalReferenceType.Course },
});

const columnNode = columnNodeFactory.buildWithId({ parent: columnBoardNode });

const cardNode = cardNodeFactory.buildWithId({ parent: columnNode });

const drawingItemNode = drawingElementNodeFactory.buildWithId({ parent: cardNode });

await em.persistAndFlush([columnBoardNode, columnNode, cardNode, drawingItemNode]);
em.clear();

const loggedInClient = await testApiClient.login(studentAccount);

return { loggedInClient, studentUser, columnBoardNode, columnNode, cardNode, drawingItemNode };
};

it('should return status 403 for student not assigned to course', async () => {
const { loggedInClient, drawingItemNode } = await setup();

const response = await loggedInClient.get(`${drawingItemNode.id}/permission`);

expect(response.status).toEqual(403);
});
});

describe('when asking for non-existing resource', () => {
const setup = async () => {
await cleanupCollections(em);

const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher();
await em.persistAndFlush([teacherAccount, teacherUser]);

em.clear();

const loggedInClient = await testApiClient.login(teacherAccount);

return { loggedInClient };
};

it('should return status 404 for wrong id', async () => {
blazejpass marked this conversation as resolved.
Show resolved Hide resolved
const { loggedInClient } = await setup();
const wrongRandomId = '655b048616056135293d1e63';

const response = await loggedInClient.get(`${wrongRandomId}/permission`);

expect(response.status).toEqual(404);
});
});
});
14 changes: 14 additions & 0 deletions apps/server/src/modules/board/controller/element.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Controller,
Delete,
ForbiddenException,
Get,
HttpCode,
NotFoundException,
Param,
Expand Down Expand Up @@ -141,4 +142,17 @@ export class ElementController {

return response;
}

@ApiOperation({ summary: 'Check if user has read permission for any board element.' })
@ApiResponse({ status: 200 })
@ApiResponse({ status: 400, type: ApiValidationError })
@ApiResponse({ status: 403, type: ForbiddenException })
@ApiResponse({ status: 404, type: NotFoundException })
@Get(':contentElementId/permission')
async readPermission(
@Param() urlParams: ContentElementUrlParams,
@CurrentUser() currentUser: ICurrentUser
): Promise<void> {
await this.elementUc.checkElementReadPermission(currentUser.userId, urlParams.contentElementId);
}
}
66 changes: 60 additions & 6 deletions apps/server/src/modules/board/uc/element.uc.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import { Action, AuthorizationService } from '@modules/authorization';
import { HttpService } from '@nestjs/axios';
import { ForbiddenException } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { BoardDoAuthorizable } from '@shared/domain/domainobject';
import { BoardDoAuthorizable, BoardRoles, UserRoleEnum } from '@shared/domain/domainobject';
import { InputFormat } from '@shared/domain/types';
import {
cardFactory,
columnBoardFactory,
drawingElementFactory,
fileElementFactory,
richTextElementFactory,
Expand Down Expand Up @@ -79,7 +81,7 @@ describe(ElementUc.name, () => {
const richTextElement = richTextElementFactory.build();
const content = { text: 'this has been updated', inputFormat: InputFormat.RICH_TEXT_CK5 };

const elementSpy = elementService.findById.mockResolvedValue(richTextElement);
const elementSpy = elementService.findById.mockResolvedValueOnce(richTextElement);

return { richTextElement, user, content, elementSpy };
};
Expand Down Expand Up @@ -107,7 +109,7 @@ describe(ElementUc.name, () => {
const fileElement = fileElementFactory.build();
const content = { caption: 'this has been updated', alternativeText: 'this altText has been updated' };

const elementSpy = elementService.findById.mockResolvedValue(fileElement);
const elementSpy = elementService.findById.mockResolvedValueOnce(fileElement);

return { fileElement, user, content, elementSpy };
};
Expand Down Expand Up @@ -225,7 +227,7 @@ describe(ElementUc.name, () => {
const user = userFactory.build();
const fileElement = fileElementFactory.build();

elementService.findById.mockResolvedValue(fileElement);
elementService.findById.mockResolvedValueOnce(fileElement);

return { fileElement, user };
};
Expand All @@ -246,7 +248,7 @@ describe(ElementUc.name, () => {

const submissionContainer = submissionContainerElementFactory.build({ children: [fileElement] });

elementService.findById.mockResolvedValue(submissionContainer);
elementService.findById.mockResolvedValueOnce(submissionContainer);

return { submissionContainer, fileElement, user };
};
Expand All @@ -267,7 +269,7 @@ describe(ElementUc.name, () => {
const submissionItem = submissionItemFactory.build({ userId: user.id });
const submissionContainer = submissionContainerElementFactory.build({ children: [submissionItem] });

elementService.findById.mockResolvedValue(submissionContainer);
elementService.findById.mockResolvedValueOnce(submissionContainer);

return { submissionContainer, submissionItem, user };
};
Expand All @@ -281,4 +283,56 @@ describe(ElementUc.name, () => {
});
});
});

describe('checkElementReadPermission', () => {
const setup = () => {
const user = userFactory.build();
const drawingElement = drawingElementFactory.build();
const card = cardFactory.build({ children: [drawingElement] });
const columnBoard = columnBoardFactory.build({ children: [card] });
authorizationService.getUserWithPermissions.mockResolvedValueOnce(user);

const authorizableMock: BoardDoAuthorizable = new BoardDoAuthorizable({
users: [{ userId: user.id, roles: [BoardRoles.EDITOR], userRoleEnum: UserRoleEnum.TEACHER }],
id: columnBoard.id,
});

boardDoAuthorizableService.findById.mockResolvedValueOnce(authorizableMock);

return { drawingElement, user };
};

it('should properly find the element', async () => {
const { drawingElement, user } = setup();
elementService.findById.mockResolvedValueOnce(drawingElement);

await uc.checkElementReadPermission(user.id, drawingElement.id);

expect(elementService.findById).toHaveBeenCalledWith(drawingElement.id);
});

it('should properly check element permission and not throw', async () => {
const { drawingElement, user } = setup();
elementService.findById.mockResolvedValueOnce(drawingElement);

await expect(uc.checkElementReadPermission(user.id, drawingElement.id)).resolves.not.toThrow();
});

it('should throw at find element by Id', async () => {
const { drawingElement, user } = setup();
elementService.findById.mockRejectedValueOnce(new Error());

await expect(uc.checkElementReadPermission(user.id, drawingElement.id)).rejects.toThrow();
});

it('should throw at check permission', async () => {
const { user } = setup();
const testElementId = 'wrongTestId123';
authorizationService.checkPermission.mockImplementationOnce(() => {
throw new Error();
});

await expect(uc.checkElementReadPermission(user.id, testElementId)).rejects.toThrow();
});
});
});
5 changes: 5 additions & 0 deletions apps/server/src/modules/board/uc/element.uc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ export class ElementUc extends BaseUc {
return element;
}

async checkElementReadPermission(userId: EntityId, elementId: EntityId): Promise<void> {
const element = await this.elementService.findById(elementId);
await this.checkPermission(userId, element, Action.read);
}

async createSubmissionItem(
userId: EntityId,
contentElementId: EntityId,
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/modules/tldraw/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface TldrawConfig {
FEATURE_TLDRAW_ENABLED: boolean;
TLDRAW_PING_TIMEOUT: number;
TLDRAW_GC_ENABLED: number;
API_HOST: number;
}

const tldrawConnectionString: string = Configuration.get('TLDRAW_DB_URL') as string;
Expand All @@ -24,6 +25,7 @@ const tldrawConfig = {
CONNECTION_STRING: tldrawConnectionString,
TLDRAW_PING_TIMEOUT: Configuration.get('TLDRAW__PING_TIMEOUT') as number,
TLDRAW_GC_ENABLED: Configuration.get('TLDRAW__GC_ENABLED') as boolean,
API_HOST: Configuration.get('API_HOST') as string,
};

export const SOCKET_PORT = Configuration.get('TLDRAW__SOCKET_PORT') as number;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { INestApplication } from '@nestjs/common';
import { EntityManager } from '@mikro-orm/mongodb';
import { cleanupCollections, courseFactory, TestApiClient, UserAndAccountTestFactory } from '@shared/testing';
import { Test, TestingModule } from '@nestjs/testing';
import { ServerTestModule } from '@modules/server';
import { Logger } from '@src/core/logger';
import { TldrawService } from '../../service';
import { TldrawController } from '..';
import { TldrawRepo } from '../../repo';
import { tldrawEntityFactory } from '../../factory';

const baseRouteName = '/tldraw-document';
describe('tldraw controller (api)', () => {
let app: INestApplication;
let em: EntityManager;
let testApiClient: TestApiClient;

beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [ServerTestModule],
controllers: [TldrawController],
providers: [Logger, TldrawService, TldrawRepo],
}).compile();

app = module.createNestApplication();
await app.init();
em = module.get(EntityManager);
testApiClient = new TestApiClient(app, baseRouteName);
});

afterAll(async () => {
await app.close();
});

describe('with valid user', () => {
const setup = async () => {
await cleanupCollections(em);

const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher();
const course = courseFactory.build({ teachers: [teacherUser] });
await em.persistAndFlush([teacherAccount, teacherUser, course]);

const drawingItemData = tldrawEntityFactory.build();

await em.persistAndFlush([drawingItemData]);
em.clear();

const loggedInClient = await testApiClient.login(teacherAccount);

return { loggedInClient, teacherUser, drawingItemData };
};

it('should return status 200 for delete', async () => {
const { loggedInClient, drawingItemData } = await setup();

const response = await loggedInClient.delete(`${drawingItemData.docName}`);

expect(response.status).toEqual(204);
});

it('should return status 404 for delete with wrong id', async () => {
const { loggedInClient } = await setup();

const response = await loggedInClient.delete(`testID123`);

expect(response.status).toEqual(404);
});
});
});
Loading
Loading