Skip to content

Commit

Permalink
♻️ backend: move editing_session_key generation and parsing to specif…
Browse files Browse the repository at this point in the history
…ic implementation (#525)
  • Loading branch information
ericlinagora committed Jul 21, 2024
1 parent df607b1 commit 8a30a44
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 27 deletions.
2 changes: 1 addition & 1 deletion Documentation/docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
81 changes: 79 additions & 2 deletions tdrive/backend/node/src/services/documents/entities/drive-file.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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")
Expand All @@ -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;
Expand Down
32 changes: 8 additions & 24 deletions tdrive/backend/node/src/services/documents/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -58,7 +58,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";

Expand Down Expand Up @@ -936,33 +935,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);
Expand Down

0 comments on commit 8a30a44

Please sign in to comment.