🏗️(y-provider) manage auth in y-provider app
The way to connect to the hocuspocus server needs to be proxified in nginx to query a dedicated route in the django application and then follow the request to the express server with the additionnal headers. The auth can be done in the express server by querying the backend on the document retrieve endpoint. If the response status code is 200, the user has access to the document, otherwise it is not the case. Then we can check the abilities to determine what the user can do or not.
This commit is contained in:
@@ -13,11 +13,22 @@ jest.mock('../src/env', () => {
|
||||
PORT: port,
|
||||
COLLABORATION_SERVER_ORIGIN: origin,
|
||||
COLLABORATION_SERVER_SECRET: 'test-secret-api-key',
|
||||
COLLABORATION_BACKEND_BASE_URL: 'http://app-dev:8000',
|
||||
};
|
||||
});
|
||||
|
||||
console.error = jest.fn();
|
||||
|
||||
const mockDocFetch = jest.fn();
|
||||
jest.mock('@/api/getDoc', () => ({
|
||||
fetchDocument: mockDocFetch,
|
||||
}));
|
||||
|
||||
const mockGetMe = jest.fn();
|
||||
jest.mock('@/api/getMe', () => ({
|
||||
getMe: mockGetMe,
|
||||
}));
|
||||
|
||||
import { hocusPocusServer } from '@/servers/hocusPocusServer';
|
||||
|
||||
import { promiseDone } from '../src/helpers';
|
||||
@@ -30,40 +41,15 @@ describe('Server Tests', () => {
|
||||
await hocusPocusServer.configure({ port: portWS }).listen();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
server.close();
|
||||
void hocusPocusServer.destroy();
|
||||
});
|
||||
|
||||
test('WebSocket connection with correct API key can connect', () => {
|
||||
const { promise, done } = promiseDone();
|
||||
|
||||
// eslint-disable-next-line jest/unbound-method
|
||||
const { handleConnection } = hocusPocusServer;
|
||||
const mockHandleConnection = jest.fn();
|
||||
(hocusPocusServer.handleConnection as jest.Mock) = mockHandleConnection;
|
||||
|
||||
const clientWS = new WebSocket(
|
||||
`ws://localhost:${port}/collaboration/ws/?room=test-room`,
|
||||
{
|
||||
headers: {
|
||||
authorization: 'test-secret-api-key',
|
||||
Origin: origin,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
clientWS.on('open', () => {
|
||||
expect(mockHandleConnection).toHaveBeenCalled();
|
||||
clientWS.close();
|
||||
mockHandleConnection.mockClear();
|
||||
hocusPocusServer.handleConnection = handleConnection;
|
||||
done();
|
||||
});
|
||||
|
||||
return promise;
|
||||
});
|
||||
|
||||
test('WebSocket connection with bad origin should be closed', () => {
|
||||
const { promise, done } = promiseDone();
|
||||
|
||||
@@ -84,13 +70,13 @@ describe('Server Tests', () => {
|
||||
return promise;
|
||||
});
|
||||
|
||||
test('WebSocket connection with incorrect API key should be closed', () => {
|
||||
test('WebSocket connection without cookies header should be closed', () => {
|
||||
const { promise, done } = promiseDone();
|
||||
|
||||
const ws = new WebSocket(
|
||||
`ws://localhost:${port}/collaboration/ws/?room=test-room`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: 'wrong-api-key',
|
||||
Origin: origin,
|
||||
},
|
||||
},
|
||||
@@ -121,6 +107,12 @@ describe('Server Tests', () => {
|
||||
quiet: true,
|
||||
preserveConnection: false,
|
||||
onClose: (data) => {
|
||||
expect(console.error).toHaveBeenCalledWith(
|
||||
'Invalid room name - Probable hacking attempt:',
|
||||
'hocuspocus-test',
|
||||
'my-test',
|
||||
);
|
||||
|
||||
wsHocus.stopConnectionAttempt();
|
||||
expect(data.event.reason).toBe('Forbidden');
|
||||
wsHocus.webSocket?.close();
|
||||
@@ -134,9 +126,142 @@ describe('Server Tests', () => {
|
||||
return promise;
|
||||
});
|
||||
|
||||
test('WebSocket connection read-only', () => {
|
||||
test('WebSocket connection fails if user can not access document', () => {
|
||||
const { promise, done } = promiseDone();
|
||||
|
||||
mockDocFetch.mockRejectedValue('');
|
||||
|
||||
const wsHocus = new HocuspocusProviderWebsocket({
|
||||
url: `ws://localhost:${portWS}/?room=my-test`,
|
||||
WebSocketPolyfill: WebSocket,
|
||||
maxAttempts: 1,
|
||||
quiet: true,
|
||||
});
|
||||
|
||||
const provider = new HocuspocusProvider({
|
||||
websocketProvider: wsHocus,
|
||||
name: 'my-test',
|
||||
broadcast: false,
|
||||
quiet: true,
|
||||
preserveConnection: false,
|
||||
onClose: (data) => {
|
||||
expect(console.error).toHaveBeenCalledWith(
|
||||
'[onConnect]',
|
||||
'Backend error: Unauthorized',
|
||||
);
|
||||
|
||||
wsHocus.stopConnectionAttempt();
|
||||
expect(data.event.reason).toBe('Forbidden');
|
||||
expect(mockDocFetch).toHaveBeenCalledTimes(1);
|
||||
wsHocus.webSocket?.close();
|
||||
wsHocus.disconnect();
|
||||
provider.destroy();
|
||||
wsHocus.destroy();
|
||||
done();
|
||||
},
|
||||
});
|
||||
|
||||
return promise;
|
||||
});
|
||||
|
||||
test('WebSocket connection fails if user do not have correct retrieve ability', () => {
|
||||
const { promise, done } = promiseDone();
|
||||
|
||||
mockDocFetch.mockResolvedValue({
|
||||
abilities: {
|
||||
retrieve: false,
|
||||
},
|
||||
});
|
||||
|
||||
const wsHocus = new HocuspocusProviderWebsocket({
|
||||
url: `ws://localhost:${portWS}/?room=my-test`,
|
||||
WebSocketPolyfill: WebSocket,
|
||||
maxAttempts: 1,
|
||||
quiet: true,
|
||||
});
|
||||
|
||||
const provider = new HocuspocusProvider({
|
||||
websocketProvider: wsHocus,
|
||||
name: 'my-test',
|
||||
broadcast: false,
|
||||
quiet: true,
|
||||
preserveConnection: false,
|
||||
onClose: (data) => {
|
||||
expect(console.error).toHaveBeenCalledWith(
|
||||
'onConnect: Unauthorized to retrieve this document',
|
||||
'my-test',
|
||||
);
|
||||
|
||||
wsHocus.stopConnectionAttempt();
|
||||
expect(data.event.reason).toBe('Forbidden');
|
||||
expect(mockDocFetch).toHaveBeenCalledTimes(1);
|
||||
wsHocus.webSocket?.close();
|
||||
wsHocus.disconnect();
|
||||
provider.destroy();
|
||||
wsHocus.destroy();
|
||||
done();
|
||||
},
|
||||
});
|
||||
|
||||
return promise;
|
||||
});
|
||||
|
||||
[true, false].forEach((canEdit) => {
|
||||
test(`WebSocket connection ${canEdit ? 'can' : 'can not'} edit document`, () => {
|
||||
const { promise, done } = promiseDone();
|
||||
|
||||
mockDocFetch.mockResolvedValue({
|
||||
abilities: {
|
||||
retrieve: true,
|
||||
update: canEdit,
|
||||
},
|
||||
});
|
||||
|
||||
const wsHocus = new HocuspocusProviderWebsocket({
|
||||
url: `ws://localhost:${portWS}/?room=hocuspocus-test`,
|
||||
WebSocketPolyfill: WebSocket,
|
||||
});
|
||||
|
||||
const provider = new HocuspocusProvider({
|
||||
websocketProvider: wsHocus,
|
||||
name: 'hocuspocus-test',
|
||||
broadcast: false,
|
||||
quiet: true,
|
||||
onConnect: () => {
|
||||
void hocusPocusServer
|
||||
.openDirectConnection('hocuspocus-test')
|
||||
.then((connection) => {
|
||||
connection.document?.getConnections().forEach((connection) => {
|
||||
expect(connection.readOnly).toBe(!canEdit);
|
||||
});
|
||||
|
||||
void connection.disconnect();
|
||||
|
||||
provider.destroy();
|
||||
wsHocus.destroy();
|
||||
done();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return promise;
|
||||
});
|
||||
});
|
||||
|
||||
test('Add request header x-user-id if found', () => {
|
||||
const { promise, done } = promiseDone();
|
||||
|
||||
mockDocFetch.mockResolvedValue({
|
||||
abilities: {
|
||||
retrieve: true,
|
||||
update: true,
|
||||
},
|
||||
});
|
||||
|
||||
mockGetMe.mockResolvedValue({
|
||||
id: 'test-user-id',
|
||||
});
|
||||
|
||||
const wsHocus = new HocuspocusProviderWebsocket({
|
||||
url: `ws://localhost:${portWS}/?room=hocuspocus-test`,
|
||||
WebSocketPolyfill: WebSocket,
|
||||
@@ -152,15 +277,14 @@ describe('Server Tests', () => {
|
||||
.openDirectConnection('hocuspocus-test')
|
||||
.then((connection) => {
|
||||
connection.document?.getConnections().forEach((connection) => {
|
||||
expect(connection.readOnly).toBe(true);
|
||||
expect(connection.context.userId).toBe('test-user-id');
|
||||
});
|
||||
|
||||
void connection.disconnect();
|
||||
provider.destroy();
|
||||
wsHocus.destroy();
|
||||
done();
|
||||
});
|
||||
|
||||
provider.destroy();
|
||||
wsHocus.destroy();
|
||||
done();
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user