♻️ backend: adding freeform instanceid to identify multiple instances of plugin (#525)

This commit is contained in:
Eric Doughty-Papassideris
2024-09-03 21:03:19 +02:00
parent 483b1b0688
commit afe6562804
4 changed files with 34 additions and 18 deletions
@@ -135,6 +135,14 @@ const OnlyOfficeSafeDocKeyBase64 = {
}, },
}; };
function checkFieldValue(field: string, value: string) {
if (!/^[0-9a-zA-Z_-]+$/m.test(value))
throw new Error(
`Invalid ${field} value (${JSON.stringify(
value,
)}). Must be short and only alpha numeric or '_' and '-'`,
);
}
/** Reference implementation for generating then parsing the {@link DriveFile.editing_session_key} field */ /** Reference implementation for generating then parsing the {@link DriveFile.editing_session_key} field */
export const EditingSessionKeyFormat = { export const EditingSessionKeyFormat = {
// OnlyOffice key limits: 128 chars, [0-9a-zA-Z.=_-] // OnlyOffice key limits: 128 chars, [0-9a-zA-Z.=_-]
@@ -145,13 +153,15 @@ export const EditingSessionKeyFormat = {
// common denominator to all plugin/interop systems. Plugins that // common denominator to all plugin/interop systems. Plugins that
// require something even stricter have the option of maintaining // require something even stricter have the option of maintaining
// a look up table to an acceptable value. // a look up table to an acceptable value.
generate(applicationId: string, companyId: string, userId: string, overrideTimeStamp?: Date) { generate(
if (!/^[0-9a-zA-Z_-]+$/m.test(applicationId)) applicationId: string,
throw new Error( instanceId: string,
`Invalid applicationId string (${JSON.stringify( companyId: string,
applicationId, userId: string,
)}). Must be short and only alpha numeric`, overrideTimeStamp?: Date,
); ) {
checkFieldValue("applicationId", applicationId);
checkFieldValue("instanceId", instanceId);
const isoUTCDateNoSpecialCharsNoMS = (overrideTimeStamp ?? new Date()) const isoUTCDateNoSpecialCharsNoMS = (overrideTimeStamp ?? new Date())
.toISOString() .toISOString()
.replace(/\..+$/, "") .replace(/\..+$/, "")
@@ -162,7 +172,7 @@ export const EditingSessionKeyFormat = {
const idsString = OnlyOfficeSafeDocKeyBase64.fromBuffer( const idsString = OnlyOfficeSafeDocKeyBase64.fromBuffer(
Buffer.concat([companyIdBuffer, userIdBuffer, entropyBuffer]), Buffer.concat([companyIdBuffer, userIdBuffer, entropyBuffer]),
); );
const newKey = [isoUTCDateNoSpecialCharsNoMS, applicationId, idsString].join("="); const newKey = [isoUTCDateNoSpecialCharsNoMS, applicationId, instanceId, idsString].join("=");
if (newKey.length > 128 || !/^[0-9a-zA-Z=_-]+$/m.test(newKey)) if (newKey.length > 128 || !/^[0-9a-zA-Z=_-]+$/m.test(newKey))
throw new Error( throw new Error(
`Invalid generated editingSessionKey (${JSON.stringify( `Invalid generated editingSessionKey (${JSON.stringify(
@@ -174,14 +184,14 @@ export const EditingSessionKeyFormat = {
parse(editingSessionKey: string) { parse(editingSessionKey: string) {
const parts = editingSessionKey.split("="); const parts = editingSessionKey.split("=");
const expectedParts = 3; const expectedParts = 4;
if (parts.length !== expectedParts) if (parts.length !== expectedParts)
throw new Error( throw new Error(
`Invalid editingSessionKey (${JSON.stringify( `Invalid editingSessionKey (${JSON.stringify(
editingSessionKey, editingSessionKey,
)}). Expected ${expectedParts} parts`, )}). Expected ${expectedParts} parts`,
); );
const [timestampStr, appId, idsOOBase64String] = parts; const [timestampStr, applicationId, instanceId, idsOOBase64String] = parts;
const timestampMatch = timestampStr.match( const timestampMatch = timestampStr.match(
/^(?<year>\d{4})(?<month>\d\d)(?<day>\d\d)(?<hour>\d\d)(?<minute>\d\d)(?<second>\d\d)$/, /^(?<year>\d{4})(?<month>\d\d)(?<day>\d\d)(?<hour>\d\d)(?<minute>\d\d)(?<second>\d\d)$/,
); );
@@ -199,7 +209,8 @@ export const EditingSessionKeyFormat = {
timestamp: new Date( timestamp: new Date(
Date.parse(`${[year, month, day].join("-")}T${[hour, minute, second].join(":")}Z`), Date.parse(`${[year, month, day].join("-")}T${[hour, minute, second].join(":")}Z`),
), ),
applicationId: appId, applicationId,
instanceId,
companyId, companyId,
userId, userId,
}; };
@@ -967,6 +967,7 @@ export class DocumentsService {
beginEditing = async ( beginEditing = async (
id: string, id: string,
editorApplicationId: string, editorApplicationId: string,
appInstanceId: string,
context: DriveExecutionContext, context: DriveExecutionContext,
) => { ) => {
if (!context) { if (!context) {
@@ -978,6 +979,7 @@ export class DocumentsService {
try { try {
newKey = EditingSessionKeyFormat.generate( newKey = EditingSessionKeyFormat.generate(
editorApplicationId, editorApplicationId,
appInstanceId,
context.company.id, context.company.id,
context.user.id, context.user.id,
); );
@@ -351,7 +351,7 @@ export class DocumentsController {
request: FastifyRequest<{ request: FastifyRequest<{
Params: ItemRequestParams; Params: ItemRequestParams;
//TODO application id should be received from the token that we have during the login //TODO application id should be received from the token that we have during the login
Body: { editorApplicationId: string }; Body: { editorApplicationId: string; instanceId: string };
}>, }>,
) => { ) => {
try { try {
@@ -366,6 +366,7 @@ export class DocumentsController {
return await globalResolver.services.documents.documents.beginEditing( return await globalResolver.services.documents.documents.beginEditing(
id, id,
request.body.editorApplicationId, request.body.editorApplicationId,
request.body.instanceId || "",
context, context,
); );
} catch (error) { } catch (error) {
@@ -7,6 +7,7 @@ describe('DriveFile EditingSessionKeyFormat', () => {
const mockAppId = 'tdrive_random_application_id'; const mockAppId = 'tdrive_random_application_id';
const mockCompanyId = randomUUID(); const mockCompanyId = randomUUID();
const mockUserId = randomUUID(); const mockUserId = randomUUID();
const mockInstanceId = "super-instance-id";
const mockTimestamp = new Date(); const mockTimestamp = new Date();
const mockTimestampWith0MS = mockTimestamp.getTime() - (mockTimestamp.getTime() % 1000); const mockTimestampWith0MS = mockTimestamp.getTime() - (mockTimestamp.getTime() % 1000);
@@ -23,31 +24,32 @@ describe('DriveFile EditingSessionKeyFormat', () => {
test('generates a valid value that can be parsed', async () => { test('generates a valid value that can be parsed', async () => {
checkIsUUID(mockUserId); checkIsUUID(mockUserId);
checkIsUUID(mockCompanyId); checkIsUUID(mockCompanyId);
const key = EditingSessionKeyFormat.generate(mockAppId, mockCompanyId, mockUserId, mockTimestamp); const key = EditingSessionKeyFormat.generate(mockAppId, mockInstanceId, mockCompanyId, mockUserId, mockTimestamp);
checkKeyIsOOCompatible(key); checkKeyIsOOCompatible(key);
const parsed = EditingSessionKeyFormat.parse(key); const parsed = EditingSessionKeyFormat.parse(key);
expect(parsed.applicationId).toBe(mockAppId); expect(parsed.applicationId).toBe(mockAppId);
expect(parsed.instanceId).toBe(mockInstanceId);
expect(parsed.userId).toBe(mockUserId); expect(parsed.userId).toBe(mockUserId);
expect(parsed.companyId).toBe(mockCompanyId); expect(parsed.companyId).toBe(mockCompanyId);
expect(parsed.timestamp.getTime()).toBe(mockTimestampWith0MS); expect(parsed.timestamp.getTime()).toBe(mockTimestampWith0MS);
}); });
test('generates unique values', async () => { test('generates unique values', async () => {
const key = EditingSessionKeyFormat.generate(mockAppId, mockCompanyId, mockUserId, mockTimestamp); const key = EditingSessionKeyFormat.generate(mockAppId, mockInstanceId, mockCompanyId, mockUserId, mockTimestamp);
const key2 = EditingSessionKeyFormat.generate(mockAppId, mockCompanyId, mockUserId, mockTimestamp); const key2 = EditingSessionKeyFormat.generate(mockAppId, mockInstanceId, mockCompanyId, mockUserId, mockTimestamp);
expect(key).not.toBe(key2); expect(key).not.toBe(key2);
}); });
test('checks the appId', async () => { test('checks the appId', async () => {
expect(() => { expect(() => {
EditingSessionKeyFormat.generate('invalid app id !', mockCompanyId, mockUserId); EditingSessionKeyFormat.generate('invalid app id !', mockInstanceId, mockCompanyId, mockUserId);
}).toThrow('Invalid applicationId string'); }).toThrow('Invalid applicationId value');
}); });
test('checks final length', async () => { test('checks final length', async () => {
expect(() => { expect(() => {
const tooLongAppID = new Array(100).join('x'); const tooLongAppID = new Array(100).join('x');
EditingSessionKeyFormat.generate(tooLongAppID, mockCompanyId, mockUserId); EditingSessionKeyFormat.generate(tooLongAppID, mockInstanceId, mockCompanyId, mockUserId);
}).toThrow('Must be <128 chars,'); }).toThrow('Must be <128 chars,');
}); });
}); });