♻️ backend: move editing_session_key generation and parsing to specific implementation (#525)
This commit is contained in:
@@ -43,7 +43,7 @@ Links in top right drop down application grid can be configured in the `applicat
|
||||
|
||||
Plugins are defined as entries under the `applications.plugins` array, with at least the following properties:
|
||||
|
||||
- `id` is mandatory and must be unique to each plugin
|
||||
- `id` is mandatory and must be unique to each plugin. It must be rather short, and alphanumeric with `_`s only.
|
||||
- `internal_domain` is the internal domain of the plugin's server, it must be the same as the one in the docker-compose.yml file for ex. or resolvable and accessible to the Twake Drive backend server.
|
||||
- `external_prefix` is the external URL prefix of the plugin exposed by the backend and proxied to the `internal_domain`.
|
||||
- `api.private_key` is the shared secret used by the plugin's server to authentify to the backend
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Type } from "class-transformer";
|
||||
import { randomUUID } from "crypto";
|
||||
import { Column, Entity } from "../../../core/platform/services/database/services/orm/decorators";
|
||||
import { DriveFileAccessLevel, publicAccessLevel } from "../types";
|
||||
import { FileVersion } from "./file-version";
|
||||
@@ -93,8 +94,9 @@ export class DriveFile {
|
||||
|
||||
/**
|
||||
* If this field is non-null, then an editing session is in progress (probably in OnlyOffice).
|
||||
* Should be in the format `timestamp-appid-hexuuid` where `appid` and `timestamp` have no `-`
|
||||
* characters.
|
||||
* Use {@see EditingSessionKeyFormat} to generate and interpret it.
|
||||
* Values should ensure that sorting lexicographically is chronological (assuming perfect clocks everywhere),
|
||||
* and that the application and user that started the edit session are retrievable.
|
||||
*/
|
||||
@Type(() => String)
|
||||
@Column("editing_session_key", "string")
|
||||
@@ -120,6 +122,81 @@ export class DriveFile {
|
||||
scope: DriveScope;
|
||||
}
|
||||
|
||||
/** 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=_-]
|
||||
// This is specific to it, but the constraint seems strict enough
|
||||
// that any other system needing such a unique identifier would find
|
||||
// this compatible. This value must be ensured to be the strictest
|
||||
// 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, userId: string) {
|
||||
if (!/^[0-9a-zA-Z_-]+$/m.test(applicationId))
|
||||
throw new Error(
|
||||
`Invalid applicationId string (${JSON.stringify(
|
||||
applicationId,
|
||||
)}). Must be short and only alpha numeric`,
|
||||
);
|
||||
const isoUTCDateNoSpecialCharsNoMS = new Date()
|
||||
.toISOString()
|
||||
.replace(/\..+$/, "")
|
||||
.replace(/[ZT:-]/g, "");
|
||||
const newKey = [
|
||||
isoUTCDateNoSpecialCharsNoMS,
|
||||
applicationId,
|
||||
userId.replace(/-+/g, ""),
|
||||
randomUUID().replace(/-+/g, ""),
|
||||
].join("=");
|
||||
if (newKey.length > 128 || !/^[0-9a-zA-Z=_-]+$/m.test(newKey))
|
||||
throw new Error(
|
||||
`Invalid generated editingSessionKey (${JSON.stringify(
|
||||
newKey,
|
||||
)}) string. Must be <128 chars, and only contain [0-9a-zA-z=_-]`,
|
||||
);
|
||||
return newKey;
|
||||
},
|
||||
|
||||
parse(editingSessionKey: string) {
|
||||
const parts = editingSessionKey.split("=");
|
||||
const expectedParts = 4;
|
||||
if (parts.length !== expectedParts)
|
||||
throw new Error(
|
||||
`Invalid editingSessionKey (${JSON.stringify(
|
||||
editingSessionKey,
|
||||
)}). Expected ${expectedParts} parts`,
|
||||
);
|
||||
const [timestampStr, appId, userId, _random] = parts;
|
||||
const timestampMatch = timestampStr.match(
|
||||
/^(?<year>\d{4})(?<month>\d\d)(?<day>\d\d)(?<hour>\d\d)(?<minute>\d\d)(?<second>\d\d)$/,
|
||||
);
|
||||
if (!timestampMatch)
|
||||
throw new Error(
|
||||
`Invalid editingSessionKey (${JSON.stringify(
|
||||
editingSessionKey,
|
||||
)}). Didn't start with valid timestamp`,
|
||||
);
|
||||
const { year, month, day, hour, minute, second } = timestampMatch.groups!;
|
||||
const userIdMatch = userId.match(
|
||||
/^([a-z0-f]{8})([a-z0-f]{4})([a-z0-f]{4})([a-z0-f]{4})([a-z0-f]{12})$/i,
|
||||
);
|
||||
if (!userIdMatch)
|
||||
throw new Error(
|
||||
`Invalid editingSessionKey (${JSON.stringify(
|
||||
editingSessionKey,
|
||||
)}). UserID has wrong number of digits`,
|
||||
);
|
||||
const [, userIdPart1, userIdPart2, userIdPart3, userIdPart4, userIdPart5] = userIdMatch;
|
||||
return {
|
||||
timestamp: new Date(
|
||||
Date.parse(`${[year, month, day].join("-")}T${[hour, minute, second].join(":")}Z`),
|
||||
),
|
||||
applicationId: appId,
|
||||
userId: [userIdPart1, userIdPart2, userIdPart3, userIdPart4, userIdPart5].join("-"),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export type AccessInformation = {
|
||||
public?: {
|
||||
token: string;
|
||||
|
||||
@@ -13,7 +13,7 @@ import { PublicFile } from "../../../services/files/entities/file";
|
||||
import globalResolver from "../../../services/global-resolver";
|
||||
import { hasCompanyAdminLevel } from "../../../utils/company";
|
||||
import gr from "../../global-resolver";
|
||||
import { DriveFile, TYPE } from "../entities/drive-file";
|
||||
import { DriveFile, EditingSessionKeyFormat, TYPE } from "../entities/drive-file";
|
||||
import { FileVersion, TYPE as FileVersionType } from "../entities/file-version";
|
||||
import User, { TYPE as UserType } from "../../user/entities/user";
|
||||
|
||||
@@ -60,7 +60,6 @@ import {
|
||||
import archiver from "archiver";
|
||||
import internal from "stream";
|
||||
import config from "config";
|
||||
import { randomUUID } from "crypto";
|
||||
import { MultipartFile } from "@fastify/multipart";
|
||||
import { UploadOptions } from "src/services/files/types";
|
||||
|
||||
@@ -970,33 +969,18 @@ export class DocumentsService {
|
||||
editorApplicationId: string,
|
||||
context: DriveExecutionContext,
|
||||
) => {
|
||||
const isoUTCDateNoSpecialCharsNoMS = new Date()
|
||||
.toISOString()
|
||||
.replace(/\..+$/, "")
|
||||
.replace(/[ZT:-]/g, "");
|
||||
const newKey = [
|
||||
isoUTCDateNoSpecialCharsNoMS,
|
||||
editorApplicationId,
|
||||
randomUUID().replace(/-+/g, ""),
|
||||
].join("-");
|
||||
// OnlyOffice key limits: 128 chars, [0-9a-zA-z=_-]
|
||||
// This is specific to it, but the constraint seems strict enough
|
||||
// that any other system needing such a unique identifier would find
|
||||
// this compatible. This value must be ensured to be the strictest
|
||||
// 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.
|
||||
if (newKey.length > 128 || !/^[0-9a-zA-Z=_]+$/m.test(editorApplicationId))
|
||||
CrudException.throwMe(
|
||||
new Error('Invalid "editorApplicationId" string. Must be short and only alpha numeric'),
|
||||
new CrudException("Invalid editorApplicationId", 400),
|
||||
);
|
||||
|
||||
if (!context) {
|
||||
this.logger.error("invalid execution context");
|
||||
return null;
|
||||
}
|
||||
|
||||
let newKey: string;
|
||||
try {
|
||||
newKey = EditingSessionKeyFormat.generate(editorApplicationId, context.user.id);
|
||||
} catch (e) {
|
||||
CrudException.throwMe(e, new CrudException("Error generating new editing_session_key", 500));
|
||||
}
|
||||
|
||||
const hasAccess = await checkAccess(id, null, "write", this.repository, context);
|
||||
if (!hasAccess) {
|
||||
logger.error("user does not have access drive item " + id);
|
||||
|
||||
Reference in New Issue
Block a user