🍄 Separate trash for the personal and shared drive (#189)
This commit is contained in:
+1
-17
@@ -5,7 +5,7 @@ import { defer, Subject, throwError, timer } from "rxjs";
|
||||
import { concat, delayWhen, retryWhen, take, tap } from "rxjs/operators";
|
||||
import { UpsertOptions } from "..";
|
||||
import { logger } from "../../../../../../framework";
|
||||
import { getEntityDefinition, unwrapIndexes, unwrapPrimarykey } from "../../utils";
|
||||
import { getEntityDefinition, unwrapPrimarykey } from "../../utils";
|
||||
import { EntityDefinition, ColumnDefinition, ObjectType } from "../../types";
|
||||
import { AbstractConnector } from "../abstract-connector";
|
||||
import {
|
||||
@@ -451,22 +451,6 @@ export class CassandraConnector extends AbstractConnector<
|
||||
const instance = new (entityType as any)();
|
||||
const { columnsDefinition, entityDefinition } = getEntityDefinition(instance);
|
||||
|
||||
const pk = unwrapPrimarykey(entityDefinition);
|
||||
const indexes = unwrapIndexes(entityDefinition);
|
||||
|
||||
if (
|
||||
Object.keys(filters).some(key => pk.indexOf(key) < 0) &&
|
||||
Object.keys(filters).some(key => indexes.indexOf(key) < 0)
|
||||
) {
|
||||
//Filter not in primary key
|
||||
throw new Error(
|
||||
`All filter parameters must be defined in entity primary key,
|
||||
got: ${JSON.stringify(Object.keys(filters))}
|
||||
on table ${entityDefinition.name} but pk is ${JSON.stringify(pk)},
|
||||
instance was ${JSON.stringify(instance)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const query = buildSelectQuery<Table>(
|
||||
entityType as unknown as ObjectType<Table>,
|
||||
filters,
|
||||
|
||||
+7
-2
@@ -1,6 +1,6 @@
|
||||
import { FindOptions } from "../../repository/repository";
|
||||
import { ObjectType } from "../../types";
|
||||
import { getEntityDefinition, secureOperators } from "../../utils";
|
||||
import { getEntityDefinition, secureOperators, filteringRequired } from "../../utils";
|
||||
import { transformValueToDbString } from "./typeTransforms";
|
||||
|
||||
export function buildSelectQuery<Entity>(
|
||||
@@ -17,12 +17,17 @@ export function buildSelectQuery<Entity>(
|
||||
): string {
|
||||
const instance = new (entityType as any)();
|
||||
const { columnsDefinition, entityDefinition } = getEntityDefinition(instance);
|
||||
let allowFiltering = false;
|
||||
|
||||
const where = Object.keys(filters)
|
||||
.map(key => {
|
||||
let result: string;
|
||||
const filter = filters[key];
|
||||
|
||||
if (filteringRequired(key)) {
|
||||
allowFiltering = true;
|
||||
}
|
||||
|
||||
if (!filter) {
|
||||
return;
|
||||
}
|
||||
@@ -77,7 +82,7 @@ export function buildSelectQuery<Entity>(
|
||||
whereClause.trim().length ? "WHERE " + whereClause : ""
|
||||
} ${orderByClause.trim().length ? "ORDER BY " + orderByClause : ""}`
|
||||
.trimEnd()
|
||||
.concat(";");
|
||||
.concat(allowFiltering ? " ALLOW FILTERING;" : ";");
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
+1
-19
@@ -3,7 +3,7 @@ import { UpsertOptions } from "..";
|
||||
import { ListResult, Paginable, Pagination } from "../../../../../../framework/api/crud-service";
|
||||
import { FindOptions } from "../../repository/repository";
|
||||
import { ColumnDefinition, EntityDefinition, ObjectType } from "../../types";
|
||||
import { getEntityDefinition, unwrapIndexes, unwrapPrimarykey } from "../../utils";
|
||||
import { getEntityDefinition, unwrapPrimarykey } from "../../utils";
|
||||
import { AbstractConnector } from "../abstract-connector";
|
||||
import { buildSelectQuery } from "./query-builder";
|
||||
import { transformValueFromDbString, transformValueToDbString } from "./typeTransforms";
|
||||
@@ -186,24 +186,6 @@ export class MongoConnector extends AbstractConnector<MongoConnectionOptions, mo
|
||||
const instance = new (entityType as any)();
|
||||
const { columnsDefinition, entityDefinition } = getEntityDefinition(instance);
|
||||
|
||||
const pk = unwrapPrimarykey(entityDefinition);
|
||||
|
||||
const indexes = unwrapIndexes(entityDefinition);
|
||||
if (
|
||||
Object.keys(filters).some(key => pk.indexOf(key) < 0) &&
|
||||
Object.keys(filters).some(key => indexes.indexOf(key) < 0)
|
||||
) {
|
||||
//Filter not in primary key
|
||||
throw Error(
|
||||
"All filter parameters must be defined in entity primary key, got: " +
|
||||
JSON.stringify(Object.keys(filters)) +
|
||||
" on table " +
|
||||
entityDefinition.name +
|
||||
" but pk is " +
|
||||
JSON.stringify(pk),
|
||||
);
|
||||
}
|
||||
|
||||
const db = await this.getDatabase();
|
||||
const collection = db.collection(`${entityDefinition.name}`);
|
||||
|
||||
|
||||
@@ -106,3 +106,12 @@ export function toMongoDbOrderable(timeuuid?: string): string {
|
||||
const time_str = [uuid_arr[2], uuid_arr[1], uuid_arr[0], uuid_arr[3], uuid_arr[4]].join("-");
|
||||
return time_str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if filtering is necessary
|
||||
* @param {string} key
|
||||
* @returns {boolean} Returns true if key is "is_in_trash" or "scope", otherwise returns false.
|
||||
*/
|
||||
export const filteringRequired = (key: string) => {
|
||||
return key === "is_in_trash" || key === "scope";
|
||||
};
|
||||
|
||||
@@ -5,9 +5,13 @@ import { FileVersion } from "./file-version";
|
||||
import search from "./drive-file.search";
|
||||
|
||||
export const TYPE = "drive_files";
|
||||
export type DriveScope = "personal" | "shared";
|
||||
|
||||
@Entity(TYPE, {
|
||||
globalIndexes: [["company_id", "parent_id"]],
|
||||
globalIndexes: [
|
||||
["company_id", "parent_id"],
|
||||
["company_id", "is_in_trash"],
|
||||
],
|
||||
primaryKey: [["company_id"], "id"],
|
||||
type: TYPE,
|
||||
search,
|
||||
@@ -73,6 +77,10 @@ export class DriveFile {
|
||||
|
||||
@Column("last_version_cache", "encoded_json")
|
||||
last_version_cache: Partial<FileVersion>;
|
||||
|
||||
@Type(() => String)
|
||||
@Column("scope", "string")
|
||||
scope: DriveScope;
|
||||
}
|
||||
|
||||
export type AccessInformation = {
|
||||
|
||||
@@ -169,28 +169,61 @@ export const getAccessLevel = async (
|
||||
repository: Repository<DriveFile>,
|
||||
context: CompanyExecutionContext & { public_token?: string; tdrive_tab_token?: string },
|
||||
): Promise<DriveFileAccessLevel | "none"> => {
|
||||
const isAdmin = !context?.user?.id || (await isCompanyAdmin(context));
|
||||
const isMember = !context?.user?.id || (await isCompanyMember(context));
|
||||
|
||||
if (!id || id === "root") {
|
||||
if (!context?.user?.id || (await isCompanyGuest(context))) {
|
||||
return "none";
|
||||
} else {
|
||||
if (isMember) return "read";
|
||||
return "manage";
|
||||
}
|
||||
}
|
||||
if (id === "trash")
|
||||
return (await isCompanyGuest(context)) || !context?.user?.id
|
||||
? "none"
|
||||
: (await isCompanyAdmin(context))
|
||||
? "manage"
|
||||
: "write";
|
||||
if (id === "shared_with_me") return "read";
|
||||
|
||||
//If it is my personal folder, I have full access
|
||||
if (id.startsWith("user_")) {
|
||||
if (await isCompanyApplication(context)) return "read";
|
||||
}
|
||||
|
||||
if (context?.user?.id) {
|
||||
/**
|
||||
* Drive root directories access management
|
||||
*/
|
||||
if (id === "user_" + context.user?.id) return "manage";
|
||||
if (await isCompanyApplication(context)) return "manage";
|
||||
return "none";
|
||||
if (id === "trash_" + context.user?.id) return "manage";
|
||||
|
||||
if (id === "root") {
|
||||
if (isAdmin) return "manage";
|
||||
|
||||
return "read";
|
||||
}
|
||||
|
||||
if (id === "trash") {
|
||||
if (isAdmin) return "manage";
|
||||
|
||||
return "read";
|
||||
}
|
||||
|
||||
if (id === "shared_with_me") return "read";
|
||||
|
||||
/**
|
||||
* Entity based access management
|
||||
*/
|
||||
|
||||
if (!item) {
|
||||
item = await repository.findOne({
|
||||
id,
|
||||
company_id: context.company.id,
|
||||
});
|
||||
}
|
||||
|
||||
if (!item) {
|
||||
throw Error("Drive item doesn't exist");
|
||||
}
|
||||
|
||||
if (item.scope === "personal" && item.creator == context.user.id) return "manage";
|
||||
|
||||
if (item.scope === "shared") {
|
||||
if (isAdmin) return "manage";
|
||||
if (isMember) return "read";
|
||||
}
|
||||
}
|
||||
|
||||
let publicToken = context.public_token;
|
||||
@@ -208,13 +241,6 @@ export const getAccessLevel = async (
|
||||
throw Error("Drive item doesn't exist");
|
||||
}
|
||||
|
||||
if (await isCompanyApplication(context)) {
|
||||
if (!id.startsWith("user_") && isMember && item.creator != context.user.id) {
|
||||
return "read";
|
||||
}
|
||||
return "manage";
|
||||
}
|
||||
|
||||
/*
|
||||
* Specific user or channel rule is applied first. Then less restrictive level will be chosen
|
||||
* between the parent folder and company accesses.
|
||||
@@ -383,6 +409,30 @@ export const getSharedByUser = (
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getItemScope = async (
|
||||
item: DriveFile | null,
|
||||
repository: Repository<DriveFile>,
|
||||
context: CompanyExecutionContext,
|
||||
): Promise<"personal" | "shared"> => {
|
||||
let scope: "personal" | "shared";
|
||||
if (item.parent_id === "user_" + context.user?.id) {
|
||||
scope = "personal";
|
||||
} else if (item.parent_id === "root") {
|
||||
scope = "shared";
|
||||
} else {
|
||||
const driveItemParent = await repository.findOne(
|
||||
{
|
||||
company_id: context.company.id,
|
||||
id: item.parent_id,
|
||||
},
|
||||
{},
|
||||
context,
|
||||
);
|
||||
scope = driveItemParent.scope;
|
||||
}
|
||||
return scope;
|
||||
};
|
||||
|
||||
const getGrantorAndThrowIfEmpty = (entity: AuthEntity) => {
|
||||
if (!entity.grantor) {
|
||||
logger.warn(`For the file permissions ${entity.id} grantor is not defined`);
|
||||
|
||||
@@ -34,6 +34,7 @@ export class DriveFileDTOBuilder {
|
||||
"access_info",
|
||||
"content_keywords",
|
||||
"creator",
|
||||
"scope",
|
||||
],
|
||||
],
|
||||
[
|
||||
|
||||
@@ -40,12 +40,14 @@ import {
|
||||
isSharedWithMeFolder,
|
||||
isVirtualFolder,
|
||||
updateItemSize,
|
||||
isInTrash,
|
||||
} from "../utils";
|
||||
import {
|
||||
checkAccess,
|
||||
getAccessLevel,
|
||||
hasAccessLevel,
|
||||
makeStandaloneAccessLevel,
|
||||
getItemScope,
|
||||
} from "./access-check";
|
||||
import { websocketEventBus } from "../../../core/platform/services/realtime/bus";
|
||||
|
||||
@@ -153,7 +155,6 @@ export class DocumentsService {
|
||||
throw Error("user does not have access to this item");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
this.logger.error({ error: `${error}` }, "Failed to grant access to the drive item");
|
||||
throw new CrudException("User does not have access to this item or its children", 401);
|
||||
}
|
||||
@@ -179,7 +180,22 @@ export class DocumentsService {
|
||||
await this.repository.find(
|
||||
{
|
||||
company_id: context.company.id,
|
||||
parent_id: id,
|
||||
...(id.includes(this.TRASH)
|
||||
? {
|
||||
is_in_trash: true,
|
||||
...(id == this.TRASH
|
||||
? {
|
||||
scope: "shared",
|
||||
}
|
||||
: {
|
||||
scope: "personal",
|
||||
creator: context?.user?.id,
|
||||
}),
|
||||
}
|
||||
: {
|
||||
parent_id: id,
|
||||
is_in_trash: false,
|
||||
}),
|
||||
},
|
||||
{},
|
||||
context,
|
||||
@@ -257,6 +273,7 @@ export class DocumentsService {
|
||||
try {
|
||||
const driveItem = getDefaultDriveItem(content, context);
|
||||
const driveItemVersion = getDefaultDriveItemVersion(version, context);
|
||||
driveItem.scope = await getItemScope(driveItem, this.repository, context);
|
||||
|
||||
const hasAccess = await checkAccess(
|
||||
driveItem.parent_id,
|
||||
@@ -387,7 +404,7 @@ export class DocumentsService {
|
||||
throw Error("content mismatch");
|
||||
}
|
||||
|
||||
const updatable = ["access_info", "name", "tags", "parent_id", "description"];
|
||||
const updatable = ["access_info", "name", "tags", "parent_id", "description", "is_in_trash"];
|
||||
|
||||
for (const key of updatable) {
|
||||
if ((content as any)[key]) {
|
||||
@@ -434,6 +451,9 @@ export class DocumentsService {
|
||||
await updateItemSize(item.parent_id, this.repository, context);
|
||||
|
||||
if (oldParent) {
|
||||
item.scope = await getItemScope(item, this.repository, context);
|
||||
this.repository.save(item);
|
||||
|
||||
await updateItemSize(oldParent, this.repository, context);
|
||||
this.notifyWebsocket(oldParent, context);
|
||||
}
|
||||
@@ -529,6 +549,7 @@ export class DocumentsService {
|
||||
|
||||
const previousParentId = item.parent_id;
|
||||
if (
|
||||
(await isInTrash(item, this.repository, context)) ||
|
||||
item.parent_id === this.TRASH ||
|
||||
(await getPath(item.parent_id, this.repository, true, context))[0].id === this.TRASH
|
||||
) {
|
||||
@@ -568,7 +589,7 @@ export class DocumentsService {
|
||||
await this.repository.remove(item);
|
||||
} else {
|
||||
//This item is not in trash, we move it to trash
|
||||
item.parent_id = this.TRASH;
|
||||
item.is_in_trash = true;
|
||||
await this.update(item.id, item, context);
|
||||
}
|
||||
await updateItemSize(previousParentId, this.repository, context);
|
||||
@@ -579,6 +600,61 @@ export class DocumentsService {
|
||||
await this.notifyWebsocket("trash", context);
|
||||
};
|
||||
|
||||
/**
|
||||
* restore a Drive Document and its children
|
||||
*
|
||||
* @param {string} id - the item id
|
||||
* @param {DriveExecutionContext} context - the execution context
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
restore = async (
|
||||
id: string | RootType | TrashType,
|
||||
item?: DriveFile,
|
||||
context?: DriveExecutionContext,
|
||||
): Promise<void> => {
|
||||
if (!id) {
|
||||
//We can't remove the root folder
|
||||
return;
|
||||
}
|
||||
|
||||
item =
|
||||
item ||
|
||||
(await this.repository.findOne({
|
||||
company_id: context.company.id,
|
||||
id,
|
||||
}));
|
||||
|
||||
if (!item) {
|
||||
this.logger.error("item to delete not found");
|
||||
throw new CrudException("Drive item not found", 404);
|
||||
}
|
||||
|
||||
try {
|
||||
if (!(await checkAccess(item.id, item, "manage", this.repository, context))) {
|
||||
this.logger.error("user does not have access drive item ", id);
|
||||
throw Error("user does not have access to this item");
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error({ error: `${error}` }, "Failed to grant access to the drive item");
|
||||
throw new CrudException("User does not have access to this item or its children", 401);
|
||||
}
|
||||
|
||||
if (isInTrash(item, this.repository, context)) {
|
||||
if (item.is_in_trash != true) {
|
||||
if (item.scope === "personal") {
|
||||
item.parent_id = "user_" + context.user.id;
|
||||
} else {
|
||||
item.parent_id = "root";
|
||||
}
|
||||
} else {
|
||||
item.is_in_trash = false;
|
||||
}
|
||||
}
|
||||
this.repository.save(item);
|
||||
|
||||
this.notifyWebsocket("trash", context);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a Drive item version
|
||||
*
|
||||
|
||||
@@ -30,7 +30,13 @@ const TRASH: TrashType = "trash";
|
||||
const SHARED_WITH_ME: SharedWithMeType = "shared_with_me";
|
||||
|
||||
export const isVirtualFolder = (id: string) => {
|
||||
return id === ROOT || id === TRASH || id.startsWith("user_") || id == SHARED_WITH_ME;
|
||||
return (
|
||||
id === ROOT ||
|
||||
id === TRASH ||
|
||||
id.startsWith("trash_") ||
|
||||
id.startsWith("user_") ||
|
||||
id == SHARED_WITH_ME
|
||||
);
|
||||
};
|
||||
|
||||
export const isSharedWithMeFolder = (id: string) => {
|
||||
@@ -72,6 +78,7 @@ export const getDefaultDriveItem = (
|
||||
last_modified: new Date().getTime(),
|
||||
parent_id: item.parent_id || "root",
|
||||
content_keywords: item.content_keywords || "",
|
||||
scope: "personal",
|
||||
description: item.description || "",
|
||||
access_info: item.access_info || {
|
||||
entities: [
|
||||
@@ -163,7 +170,17 @@ export const calculateItemSize = async (
|
||||
): Promise<number> => {
|
||||
if (item.id === "trash") {
|
||||
const trashedItems = await repository.find(
|
||||
{ company_id: context.company.id, parent_id: "trash" },
|
||||
{ company_id: context.company.id, is_in_trash: true, scope: "shared" },
|
||||
{},
|
||||
context,
|
||||
);
|
||||
|
||||
return trashedItems.getEntities().reduce((acc, curr) => acc + curr.size, 0);
|
||||
}
|
||||
|
||||
if (item.id === "trash_" + context.user.id) {
|
||||
const trashedItems = await repository.find(
|
||||
{ company_id: context.company.id, is_in_trash: true, scope: "personal" },
|
||||
{},
|
||||
context,
|
||||
);
|
||||
@@ -173,7 +190,7 @@ export const calculateItemSize = async (
|
||||
|
||||
if (isVirtualFolder(item.id) || !item) {
|
||||
const rootFolderItems = await repository.find(
|
||||
{ company_id: context.company.id, parent_id: item.id || "root" },
|
||||
{ company_id: context.company.id, parent_id: item.id || "root", is_in_trash: false },
|
||||
{},
|
||||
context,
|
||||
);
|
||||
@@ -186,6 +203,7 @@ export const calculateItemSize = async (
|
||||
{
|
||||
company_id: context.company.id,
|
||||
parent_id: item.id,
|
||||
is_in_trash: false,
|
||||
},
|
||||
{},
|
||||
context,
|
||||
@@ -561,3 +579,26 @@ export function isFileType(
|
||||
const fileExtensions = [extension, ...secondaryExtensions];
|
||||
return fileExtensions.some(e => requiredExtensions.includes(e));
|
||||
}
|
||||
|
||||
export const isInTrash = async (
|
||||
item: DriveFile,
|
||||
repository: Repository<DriveFile>,
|
||||
context: CompanyExecutionContext,
|
||||
) => {
|
||||
if (item.is_in_trash === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isVirtualFolder(item.parent_id)) {
|
||||
return false; // Stop condition
|
||||
}
|
||||
|
||||
// Retrieve the parent item
|
||||
const parentItem = await repository.findOne({
|
||||
company_id: context.company.id,
|
||||
id: item.parent_id,
|
||||
});
|
||||
|
||||
// Recursively check the parent item
|
||||
return isInTrash(parentItem, repository, context);
|
||||
};
|
||||
|
||||
@@ -114,6 +114,29 @@ export class DocumentsController {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Restore a DriveFile item from the trashÒ
|
||||
*
|
||||
* @param {FastifyRequest} request
|
||||
* @param {FastifyReply} reply
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
restore = async (
|
||||
request: FastifyRequest<{ Params: ItemRequestParams; Querystring: { public_token?: string } }>,
|
||||
reply: FastifyReply,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const context = getDriveExecutionContext(request);
|
||||
|
||||
await globalResolver.services.documents.documents.restore(request.params.id, null, context);
|
||||
|
||||
reply.status(200).send();
|
||||
} catch (error) {
|
||||
logger.error({ error: `${error}` }, "Failed to restore drive item");
|
||||
throw new CrudException("Failed to restore drive item", 500);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Lists the drive root folder.
|
||||
*
|
||||
|
||||
@@ -51,6 +51,13 @@ const routes: FastifyPluginCallback = (fastify: FastifyInstance, _options, next)
|
||||
handler: documentsController.delete.bind(documentsController),
|
||||
});
|
||||
|
||||
fastify.route({
|
||||
method: "POST",
|
||||
url: `${serviceUrl}/:id/restore`,
|
||||
preValidation: [fastify.authenticateOptional],
|
||||
handler: documentsController.restore.bind(documentsController),
|
||||
});
|
||||
|
||||
fastify.route({
|
||||
method: "GET",
|
||||
url: `${serviceUrl}/:id/user/:user_id/access`,
|
||||
|
||||
@@ -83,6 +83,7 @@ const documentSchema = {
|
||||
last_user: { type: "string" },
|
||||
attachements: { type: "array" },
|
||||
last_version_cache: fileVersionSchema,
|
||||
scope: { type: "string" },
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user