♻️ 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 */
export const EditingSessionKeyFormat = {
// 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
// require something even stricter have the option of maintaining
// a look up table to an acceptable value.
generate(applicationId: string, companyId: string, userId: string, overrideTimeStamp?: Date) {
if (!/^[0-9a-zA-Z_-]+$/m.test(applicationId))
throw new Error(
`Invalid applicationId string (${JSON.stringify(
applicationId,
)}). Must be short and only alpha numeric`,
);
generate(
applicationId: string,
instanceId: string,
companyId: string,
userId: string,
overrideTimeStamp?: Date,
) {
checkFieldValue("applicationId", applicationId);
checkFieldValue("instanceId", instanceId);
const isoUTCDateNoSpecialCharsNoMS = (overrideTimeStamp ?? new Date())
.toISOString()
.replace(/\..+$/, "")
@@ -162,7 +172,7 @@ export const EditingSessionKeyFormat = {
const idsString = OnlyOfficeSafeDocKeyBase64.fromBuffer(
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))
throw new Error(
`Invalid generated editingSessionKey (${JSON.stringify(
@@ -174,14 +184,14 @@ export const EditingSessionKeyFormat = {
parse(editingSessionKey: string) {
const parts = editingSessionKey.split("=");
const expectedParts = 3;
const expectedParts = 4;
if (parts.length !== expectedParts)
throw new Error(
`Invalid editingSessionKey (${JSON.stringify(
editingSessionKey,
)}). Expected ${expectedParts} parts`,
);
const [timestampStr, appId, idsOOBase64String] = parts;
const [timestampStr, applicationId, instanceId, idsOOBase64String] = parts;
const timestampMatch = timestampStr.match(
/^(?<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(
Date.parse(`${[year, month, day].join("-")}T${[hour, minute, second].join(":")}Z`),
),
applicationId: appId,
applicationId,
instanceId,
companyId,
userId,
};
@@ -967,6 +967,7 @@ export class DocumentsService {
beginEditing = async (
id: string,
editorApplicationId: string,
appInstanceId: string,
context: DriveExecutionContext,
) => {
if (!context) {
@@ -978,6 +979,7 @@ export class DocumentsService {
try {
newKey = EditingSessionKeyFormat.generate(
editorApplicationId,
appInstanceId,
context.company.id,
context.user.id,
);
@@ -351,7 +351,7 @@ export class DocumentsController {
request: FastifyRequest<{
Params: ItemRequestParams;
//TODO application id should be received from the token that we have during the login
Body: { editorApplicationId: string };
Body: { editorApplicationId: string; instanceId: string };
}>,
) => {
try {
@@ -366,6 +366,7 @@ export class DocumentsController {
return await globalResolver.services.documents.documents.beginEditing(
id,
request.body.editorApplicationId,
request.body.instanceId || "",
context,
);
} catch (error) {
@@ -7,6 +7,7 @@ describe('DriveFile EditingSessionKeyFormat', () => {
const mockAppId = 'tdrive_random_application_id';
const mockCompanyId = randomUUID();
const mockUserId = randomUUID();
const mockInstanceId = "super-instance-id";
const mockTimestamp = new Date();
const mockTimestampWith0MS = mockTimestamp.getTime() - (mockTimestamp.getTime() % 1000);
@@ -23,31 +24,32 @@ describe('DriveFile EditingSessionKeyFormat', () => {
test('generates a valid value that can be parsed', async () => {
checkIsUUID(mockUserId);
checkIsUUID(mockCompanyId);
const key = EditingSessionKeyFormat.generate(mockAppId, mockCompanyId, mockUserId, mockTimestamp);
const key = EditingSessionKeyFormat.generate(mockAppId, mockInstanceId, mockCompanyId, mockUserId, mockTimestamp);
checkKeyIsOOCompatible(key);
const parsed = EditingSessionKeyFormat.parse(key);
expect(parsed.applicationId).toBe(mockAppId);
expect(parsed.instanceId).toBe(mockInstanceId);
expect(parsed.userId).toBe(mockUserId);
expect(parsed.companyId).toBe(mockCompanyId);
expect(parsed.timestamp.getTime()).toBe(mockTimestampWith0MS);
});
test('generates unique values', async () => {
const key = EditingSessionKeyFormat.generate(mockAppId, mockCompanyId, mockUserId, mockTimestamp);
const key2 = EditingSessionKeyFormat.generate(mockAppId, mockCompanyId, mockUserId, mockTimestamp);
const key = EditingSessionKeyFormat.generate(mockAppId, mockInstanceId, mockCompanyId, mockUserId, mockTimestamp);
const key2 = EditingSessionKeyFormat.generate(mockAppId, mockInstanceId, mockCompanyId, mockUserId, mockTimestamp);
expect(key).not.toBe(key2);
});
test('checks the appId', async () => {
expect(() => {
EditingSessionKeyFormat.generate('invalid app id !', mockCompanyId, mockUserId);
}).toThrow('Invalid applicationId string');
EditingSessionKeyFormat.generate('invalid app id !', mockInstanceId, mockCompanyId, mockUserId);
}).toThrow('Invalid applicationId value');
});
test('checks final length', async () => {
expect(() => {
const tooLongAppID = new Array(100).join('x');
EditingSessionKeyFormat.generate(tooLongAppID, mockCompanyId, mockUserId);
EditingSessionKeyFormat.generate(tooLongAppID, mockInstanceId, mockCompanyId, mockUserId);
}).toThrow('Must be <128 chars,');
});
});