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 9 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,118 @@
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 201', async () => {
const { loggedInClient, drawingItemNode } = await setup();

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

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

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 401 for student not assigned to course', async () => {
blazejpass marked this conversation as resolved.
Show resolved Hide resolved
const { loggedInClient, drawingItemNode } = await setup();

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

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

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);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,13 @@ export class ContentElementUrlParams {
})
contentElementId!: string;
}

export class DrawingPermissionUrlParams {
@IsMongoId()
@ApiProperty({
description: 'The name of a drawing.',
required: true,
nullable: false,
})
drawingName!: string;
blazejpass marked this conversation as resolved.
Show resolved Hide resolved
}
16 changes: 16 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 All @@ -21,6 +22,7 @@ import {
CreateSubmissionItemBodyParams,
DrawingElementContentBody,
DrawingElementResponse,
DrawingPermissionUrlParams,
ExternalToolElementContentBody,
ExternalToolElementResponse,
FileElementContentBody,
Expand Down Expand Up @@ -141,4 +143,18 @@ export class ElementController {

return response;
}

@ApiOperation({ summary: 'Check permission for a drawing element.' })
blazejpass marked this conversation as resolved.
Show resolved Hide resolved
@ApiResponse({ status: 201 })
@ApiResponse({ status: 400, type: ApiValidationError })
@ApiResponse({ status: 403, type: ForbiddenException })
@ApiResponse({ status: 404, type: NotFoundException })
@HttpCode(201)
blazejpass marked this conversation as resolved.
Show resolved Hide resolved
@Get(':drawingName/permission')
async checkDrawingPermission(
blazejpass marked this conversation as resolved.
Show resolved Hide resolved
@Param() urlParams: DrawingPermissionUrlParams,
@CurrentUser() currentUser: ICurrentUser
): Promise<void> {
await this.elementUc.checkElementReadPermission(currentUser.userId, urlParams.drawingName);
}
}
24 changes: 24 additions & 0 deletions apps/server/src/modules/board/uc/element.uc.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,4 +281,28 @@ describe(ElementUc.name, () => {
});
});
});

describe('checkDrawingPermission', () => {
describe('', () => {
blazejpass marked this conversation as resolved.
Show resolved Hide resolved
const setup = () => {
const user = userFactory.build();
const drawingElement = drawingElementFactory.build();

return { drawingElement, user };
};

it('should execute properly', async () => {
const { drawingElement, user } = setup();
elementService.findById.mockResolvedValue(drawingElement);
await uc.checkElementReadPermission(user.id, drawingElement.id);
expect(elementService.findById).toHaveBeenCalledWith(drawingElement.id);
});

it('should throw', async () => {
const { drawingElement, user } = setup();
elementService.findById.mockRejectedValue(new Error());
await expect(uc.checkElementReadPermission(user.id, drawingElement.id)).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
17 changes: 15 additions & 2 deletions apps/server/src/modules/tldraw/controller/tldraw.ws.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { WebSocketGateway, WebSocketServer, OnGatewayInit, OnGatewayConnection } from '@nestjs/websockets';
import { Server, WebSocket } from 'ws';
import { Request } from 'express';
import { ConfigService } from '@nestjs/config';
import cookie from 'cookie';
import { TldrawConfig, SOCKET_PORT } from '../config';
import { WsCloseCodeEnum } from '../types';
import { TldrawWsService } from '../service';
Expand All @@ -15,9 +17,20 @@ export class TldrawWs implements OnGatewayInit, OnGatewayConnection {
private readonly tldrawWsService: TldrawWsService
) {}

public handleConnection(client: WebSocket, request: Request): void {
async handleConnection(client: WebSocket, request: Request): Promise<void> {
blazejpass marked this conversation as resolved.
Show resolved Hide resolved
const docName = this.getDocNameFromRequest(request);

const cookies = cookie.parse(request.headers.cookie || '');
if (!cookies?.jwt) {
client.close(WsCloseCodeEnum.WS_CLIENT_UNAUTHORISED_CONNECTION_CODE, 'Unauthorised connection.');
}
try {
await this.tldrawWsService.authorizeConnection(docName, cookies.jwt);
} catch (e) {
client.close(
WsCloseCodeEnum.WS_CLIENT_UNAUTHORISED_CONNECTION_CODE,
"Unauthorised connection - you don't have permission to this drawing."
);
}
if (docName.length > 0 && this.configService.get<string>('FEATURE_TLDRAW_ENABLED')) {
this.tldrawWsService.setupWSConnection(client, docName);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import * as SyncProtocols from 'y-protocols/sync';
import * as AwarenessProtocol from 'y-protocols/awareness';
import { encoding } from 'lib0';
import { TldrawWsFactory } from '@shared/testing/factory/tldraw.ws.factory';
import { HttpModule } from '@nestjs/axios';
import { WsSharedDocDo } from '../domain/ws-shared-doc.do';
import { config } from '../config';
import { TldrawBoardRepo } from '../repo';
Expand Down Expand Up @@ -48,7 +49,7 @@ describe('TldrawWSService', () => {
jest.useFakeTimers();

beforeAll(async () => {
const imports = [CoreModule, ConfigModule.forRoot(createConfigModuleOptions(config))];
const imports = [CoreModule, ConfigModule.forRoot(createConfigModuleOptions(config)), HttpModule];
const testingModule = await Test.createTestingModule({
imports,
providers: [TldrawWs, TldrawBoardRepo, TldrawWsService],
Expand Down
17 changes: 16 additions & 1 deletion apps/server/src/modules/tldraw/service/tldraw.ws.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import WebSocket from 'ws';
import { applyAwarenessUpdate, encodeAwarenessUpdate, removeAwarenessStates } from 'y-protocols/awareness';
import { encoding, decoding, map } from 'lib0';
import { readSyncMessage, writeSyncStep1, writeUpdate } from 'y-protocols/sync';
import { firstValueFrom } from 'rxjs';
import { HttpService } from '@nestjs/axios';
import { Configuration } from '@hpi-schul-cloud/commons/lib';
import { Persitence, WSConnectionState, WSMessageType } from '../types';
import { TldrawConfig } from '../config';
import { WsSharedDocDo } from '../domain/ws-shared-doc.do';
Expand All @@ -19,7 +22,8 @@ export class TldrawWsService {

constructor(
private readonly configService: ConfigService<TldrawConfig, true>,
private readonly tldrawBoardRepo: TldrawBoardRepo
private readonly tldrawBoardRepo: TldrawBoardRepo,
private readonly httpService: HttpService
) {
this.pingTimeout = this.configService.get<number>('TLDRAW_PING_TIMEOUT');
}
Expand Down Expand Up @@ -206,4 +210,15 @@ export class TldrawWsService {
public async flushDocument(docName: string): Promise<void> {
await this.tldrawBoardRepo.flushDocument(docName);
}

async authorizeConnection(drawingName: string, token: string) {
blazejpass marked this conversation as resolved.
Show resolved Hide resolved
blazejpass marked this conversation as resolved.
Show resolved Hide resolved
blazejpass marked this conversation as resolved.
Show resolved Hide resolved
await firstValueFrom(
virgilchiriac marked this conversation as resolved.
Show resolved Hide resolved
this.httpService.get(`${Configuration.get('HOST') as string}/api/v3/elements/${drawingName}/permission`, {
blazejpass marked this conversation as resolved.
Show resolved Hide resolved
headers: {
Accept: 'Application/json',
Authorization: `Bearer ${token}`,
},
})
);
}
}
3 changes: 2 additions & 1 deletion apps/server/src/modules/tldraw/tldraw-ws-test.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import { MongoMemoryDatabaseModule, MongoDatabaseModuleOptions } from '@infra/da
import { CoreModule } from '@src/core';
import { ConfigModule } from '@nestjs/config';
import { createConfigModuleOptions } from '@src/config';
import { HttpModule } from '@nestjs/axios';
import { TldrawBoardRepo } from './repo';
import { TldrawWsService } from './service';
import { config } from './config';
import { TldrawWs } from './controller';

const imports = [CoreModule, ConfigModule.forRoot(createConfigModuleOptions(config))];
const imports = [CoreModule, ConfigModule.forRoot(createConfigModuleOptions(config)), HttpModule];
const providers = [TldrawWs, TldrawBoardRepo, TldrawWsService];
@Module({
imports,
Expand Down
3 changes: 2 additions & 1 deletion apps/server/src/modules/tldraw/tldraw-ws.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import { ConfigModule } from '@nestjs/config';
import { createConfigModuleOptions } from '@src/config';
import { CoreModule } from '@src/core';
import { Logger } from '@src/core/logger';
import { HttpModule } from '@nestjs/axios';
import { TldrawBoardRepo } from './repo';
import { TldrawWsService } from './service';
import { TldrawWs } from './controller';
import { config } from './config';

@Module({
imports: [CoreModule, ConfigModule.forRoot(createConfigModuleOptions(config))],
imports: [CoreModule, ConfigModule.forRoot(createConfigModuleOptions(config)), HttpModule],
providers: [Logger, TldrawWs, TldrawWsService, TldrawBoardRepo],
})
export class TldrawWsModule {}
1 change: 1 addition & 0 deletions apps/server/src/modules/tldraw/types/ws-close-code-enum.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export enum WsCloseCodeEnum {
WS_CLIENT_BAD_REQUEST_CODE = 4400,
WS_CLIENT_UNAUTHORISED_CONNECTION_CODE = 4401,
}
Loading