diff --git a/tdrive/backend/node/config/custom-environment-variables.json b/tdrive/backend/node/config/custom-environment-variables.json index 66c7ec6f5..3a4827318 100644 --- a/tdrive/backend/node/config/custom-environment-variables.json +++ b/tdrive/backend/node/config/custom-environment-variables.json @@ -128,8 +128,8 @@ "defaultLanguage": "DRIVE_DEFAULT_LANGUAGE", "defaultCompany": "DRIVE_DEFAULT_COMPANY", "defaultUserQuota": "DRIVE_DEFAULT_USER_QUOTA", - "featureSearchUsers": "ENABLE_FEATURE_SEARCH_USERS", "featureDisplayEmail": "ENABLE_FEATURE_DISPLAY_EMAIL", - "featureUserQuota": "ENABLE_FEATURE_USER_QUOTA" + "featureUserQuota": "ENABLE_FEATURE_USER_QUOTA", + "featureManageAccess": "ENABLE_FEATURE_MANAGE_ACCESS" } } diff --git a/tdrive/backend/node/config/default.json b/tdrive/backend/node/config/default.json index 3820a1f51..ab77e647c 100644 --- a/tdrive/backend/node/config/default.json +++ b/tdrive/backend/node/config/default.json @@ -68,7 +68,7 @@ "mongodb": { "uri": "mongodb://mongo:27017", "database": "tdrive" - }, + } }, "message-queue": { "// possible 'type' values are": "'amqp' or 'local'", @@ -114,9 +114,9 @@ }, "drive": { "featureSharedDrive": true, - "featureSearchUsers": true, "featureDisplayEmail": true, "featureUserQuota": false, + "featureManageAccess": true, "defaultCompany": "00000000-0000-4000-0000-000000000000", "defaultUserQuota": 200000000 }, diff --git a/tdrive/backend/node/src/services/documents/services/index.ts b/tdrive/backend/node/src/services/documents/services/index.ts index 39c85d61d..2df900b9f 100644 --- a/tdrive/backend/node/src/services/documents/services/index.ts +++ b/tdrive/backend/node/src/services/documents/services/index.ts @@ -79,6 +79,9 @@ export class DocumentsService { defaultQuota: number = config.has("drive.defaultUserQuota") ? config.get("drive.defaultUserQuota") : 0; + manageAccessEnabled: boolean = config.has("drive.featureManageAccess") + ? config.get("drive.featureManageAccess") + : false; logger: TdriveLogger = getLogger("Documents Service"); async init(): Promise { @@ -556,33 +559,38 @@ export class DocumentsService { oldParent = item.parent_id; } if (key === "access_info") { - const sharedWith = content.access_info.entities.filter( - info => - info.type === "user" && - info.id !== context.user.id && - !item.access_info.entities.find(entity => entity.id === info.id), - ); + // if manage access is disabled, we don't allow changing access level + if (!this.manageAccessEnabled) { + delete content.access_info; + } else if (content.access_info) { + const sharedWith = content.access_info.entities.filter( + info => + info.type === "user" && + info.id !== context.user.id && + !item.access_info.entities.find(entity => entity.id === info.id), + ); - item.access_info = content.access_info; + item.access_info = content.access_info; + + if (sharedWith.length > 0) { + // Notify the user that the document has been shared with them + this.logger.info("Notifying user that the document has been shared with them: ", { + sharedWith, + }); + gr.services.documents.engine.notifyDocumentShared({ + context, + item, + notificationEmitter: context.user.id, + notificationReceiver: sharedWith[0].id, + }); + } - if (sharedWith.length > 0) { - // Notify the user that the document has been shared with them - this.logger.info("Notifying user that the document has been shared with them: ", { - sharedWith, - }); - gr.services.documents.engine.notifyDocumentShared({ - context, - item, - notificationEmitter: context.user.id, - notificationReceiver: sharedWith[0].id, + item.access_info.entities.forEach(info => { + if (!info.grantor) { + info.grantor = context.user.id; + } }); } - - item.access_info.entities.forEach(info => { - if (!info.grantor) { - info.grantor = context.user.id; - } - }); } else if (key === "name") { renamedTo = item.name = await getItemName( content.parent_id || item.parent_id, diff --git a/tdrive/backend/node/src/services/user/utils.ts b/tdrive/backend/node/src/services/user/utils.ts index f160be80c..65f46ae41 100644 --- a/tdrive/backend/node/src/services/user/utils.ts +++ b/tdrive/backend/node/src/services/user/utils.ts @@ -54,10 +54,6 @@ export function formatCompany( [CompanyFeaturesEnum.CHAT_EDIT_FILES]: true, [CompanyFeaturesEnum.CHAT_UNLIMITED_STORAGE]: true, [CompanyFeaturesEnum.COMPANY_INVITE_MEMBER]: true, - // use the config value for this one - [CompanyFeaturesEnum.COMPANY_SEARCH_USERS]: JSON.parse( - config.get("drive.featureSearchUsers") || "true", - ), [CompanyFeaturesEnum.COMPANY_SHARED_DRIVE]: JSON.parse( config.get("drive.featureSharedDrive") || "true", ), @@ -67,6 +63,9 @@ export function formatCompany( [CompanyFeaturesEnum.COMPANY_USER_QUOTA]: JSON.parse( config.get("drive.featureUserQuota") || "false", ), + [CompanyFeaturesEnum.COMPANY_MANAGE_ACCESS]: JSON.parse( + config.get("drive.featureManageAccess") || "true", + ), }, { ...(res.plan?.features || {}), @@ -76,8 +75,6 @@ export function formatCompany( }, ); - console.log("🚀🚀 res.plan.features: ", res.plan.features); - return res; } diff --git a/tdrive/backend/node/src/services/user/web/schemas.ts b/tdrive/backend/node/src/services/user/web/schemas.ts index bb3e2fbb2..9ee0fe4fc 100644 --- a/tdrive/backend/node/src/services/user/web/schemas.ts +++ b/tdrive/backend/node/src/services/user/web/schemas.ts @@ -91,10 +91,10 @@ export const companyObjectSchema = { [CompanyFeaturesEnum.CHAT_MULTIPLE_WORKSPACES]: { type: "boolean" }, [CompanyFeaturesEnum.CHAT_UNLIMITED_STORAGE]: { type: "boolean" }, [CompanyFeaturesEnum.COMPANY_INVITE_MEMBER]: { type: "boolean" }, - [CompanyFeaturesEnum.COMPANY_SEARCH_USERS]: { type: "boolean" }, [CompanyFeaturesEnum.COMPANY_SHARED_DRIVE]: { type: "boolean" }, [CompanyFeaturesEnum.COMPANY_DISPLAY_EMAIL]: { type: "boolean" }, [CompanyFeaturesEnum.COMPANY_USER_QUOTA]: { type: "boolean" }, + [CompanyFeaturesEnum.COMPANY_MANAGE_ACCESS]: { type: "boolean" }, guests: { type: "number" }, // to rename or delete members: { type: "number" }, // to rename or delete storage: { type: "number" }, // to rename or delete diff --git a/tdrive/backend/node/src/services/user/web/types.ts b/tdrive/backend/node/src/services/user/web/types.ts index 7fa63b578..3254dce6d 100644 --- a/tdrive/backend/node/src/services/user/web/types.ts +++ b/tdrive/backend/node/src/services/user/web/types.ts @@ -83,10 +83,10 @@ export enum CompanyFeaturesEnum { CHAT_EDIT_FILES = "chat:edit_files", CHAT_UNLIMITED_STORAGE = "chat:unlimited_storage", COMPANY_INVITE_MEMBER = "company:invite_member", - COMPANY_SEARCH_USERS = "company:search_users", COMPANY_SHARED_DRIVE = "company:shared_drive", COMPANY_DISPLAY_EMAIL = "company:display_email", COMPANY_USER_QUOTA = "company:user_quota", + COMPANY_MANAGE_ACCESS = "company:managed_access", } export type CompanyFeaturesObject = { @@ -96,10 +96,10 @@ export type CompanyFeaturesObject = { [CompanyFeaturesEnum.CHAT_EDIT_FILES]?: boolean; [CompanyFeaturesEnum.CHAT_UNLIMITED_STORAGE]?: boolean; [CompanyFeaturesEnum.COMPANY_INVITE_MEMBER]?: boolean; - [CompanyFeaturesEnum.COMPANY_SEARCH_USERS]?: boolean; [CompanyFeaturesEnum.COMPANY_SHARED_DRIVE]?: boolean; [CompanyFeaturesEnum.COMPANY_DISPLAY_EMAIL]?: boolean; [CompanyFeaturesEnum.COMPANY_USER_QUOTA]?: boolean; + [CompanyFeaturesEnum.COMPANY_MANAGE_ACCESS]?: boolean; }; export type CompanyLimitsObject = { diff --git a/tdrive/backend/node/test/e2e/documents/documents-manage-access.spec.ts b/tdrive/backend/node/test/e2e/documents/documents-manage-access.spec.ts new file mode 100644 index 000000000..1067ed9ac --- /dev/null +++ b/tdrive/backend/node/test/e2e/documents/documents-manage-access.spec.ts @@ -0,0 +1,79 @@ +import { afterEach, beforeEach, describe, expect, it } from "@jest/globals"; +import { init, TestPlatform } from "../setup"; +import UserApi from "../common/user-api"; +import config from "config"; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +jest.mock("config"); + +describe("The Drive feature", () => { + let platform: TestPlatform; + let configHasSpy: jest.SpyInstance; + let configGetSpy: jest.SpyInstance; + + beforeEach(async () => { + // Mocking config to disable manage access for the drive feature + configHasSpy = jest.spyOn(config, "has"); + configGetSpy = jest.spyOn(config, "get"); + + configHasSpy.mockImplementation((setting: string) => { + return jest.requireActual("config").has(setting); + }); + configGetSpy.mockImplementation((setting: string) => { + if (setting === "drive.featureManageAccess") { + return false; // Disable manage access + } + return jest.requireActual("config").get(setting); + }); + + // Initialize platform with required services + platform = await init({ + services: [ + "webserver", + "database", + "applications", + "search", + "storage", + "message-queue", + "user", + "search", + "files", + "messages", + "auth", + "channels", + "counter", + "statistics", + "platform-services", + "documents", + ], + }); + }); + + afterEach(async () => { + // Tear down platform after each test + await platform?.tearDown(); + platform = null; + }); + + it("Shared with me should not contain files when manage access is off", async () => { + const oneUser = await UserApi.getInstance(platform, true, { companyRole: "admin" }); + const anotherUser = await UserApi.getInstance(platform, true, { companyRole: "admin" }); + + // Upload files by the uploader user + const files = await oneUser.uploadAllFilesOneByOne(); + + // Wait for file processing + await new Promise(r => setTimeout(r, 5000)); + + // Share the file with recipient user + await anotherUser.shareWithPermissions(files[1], anotherUser.user.id, "read"); + await new Promise(r => setTimeout(r, 3000)); // Wait for sharing process + + // Check if the shared file appears in recipient's "shared with me" section + const sharedDocs = await anotherUser.browseDocuments("shared_with_me"); + + // Validate that there are no shared files due to manage access being off + expect(sharedDocs.children.length).toBe(0); + }); +}); diff --git a/tdrive/frontend/src/app/features/global/services/feature-toggles-service.ts b/tdrive/frontend/src/app/features/global/services/feature-toggles-service.ts index 05d6efca3..d61f8865f 100644 --- a/tdrive/frontend/src/app/features/global/services/feature-toggles-service.ts +++ b/tdrive/frontend/src/app/features/global/services/feature-toggles-service.ts @@ -8,10 +8,10 @@ export enum FeatureNames { EDIT_FILES = 'chat:edit_files', UNLIMITED_STORAGE = 'chat:unlimited_storage', //Currently inactive COMPANY_INVITE_MEMBER = 'company:invite_member', - COMPANY_SEARCH_USERS = 'company:search_users', COMPANY_SHARED_DRIVE = 'company:shared_drive', COMPANY_DISPLAY_EMAIL = 'company:display_email', COMPANY_USER_QUOTA = 'company:user_quota', + COMPANY_MANAGE_ACCESS = 'company:managed_access', } export type FeatureValueType = boolean | number; @@ -27,10 +27,10 @@ availableFeaturesWithDefaults.set(FeatureNames.EDIT_FILES, true); availableFeaturesWithDefaults.set(FeatureNames.UNLIMITED_STORAGE, true); availableFeaturesWithDefaults.set(FeatureNames.COMPANY_INVITE_MEMBER, true); availableFeaturesWithDefaults.set(FeatureNames.COMPANY_INVITE_MEMBER, true); -availableFeaturesWithDefaults.set(FeatureNames.COMPANY_SEARCH_USERS, true); availableFeaturesWithDefaults.set(FeatureNames.COMPANY_SHARED_DRIVE, true); availableFeaturesWithDefaults.set(FeatureNames.COMPANY_DISPLAY_EMAIL, true); availableFeaturesWithDefaults.set(FeatureNames.COMPANY_USER_QUOTA, false); +availableFeaturesWithDefaults.set(FeatureNames.COMPANY_MANAGE_ACCESS, true); /** * ChannelServiceImpl that allow you to manage feature flipping in Tdrive using react feature toggles diff --git a/tdrive/frontend/src/app/views/client/body/drive/context-menu.tsx b/tdrive/frontend/src/app/views/client/body/drive/context-menu.tsx index fac62afdb..fa0007a87 100644 --- a/tdrive/frontend/src/app/views/client/body/drive/context-menu.tsx +++ b/tdrive/frontend/src/app/views/client/body/drive/context-menu.tsx @@ -27,6 +27,9 @@ import useRouterCompany from '@features/router/hooks/use-router-company'; import _, { set } from 'lodash'; import Languages from 'features/global/services/languages-service'; import { hasAnyPublicLinkAccess } from '@features/files/utils/access-info-helpers'; +import FeatureTogglesService, { + FeatureNames, +} from '@features/global/services/feature-toggles-service'; /** * This will build the context menu in different contexts @@ -89,7 +92,11 @@ export const useOnBuildContextMenu = ( type: 'menu', icon: 'users-alt', text: Languages.t('components.item_context_menu.manage_access'), - hide: access === 'read' || getPublicLinkToken() || inTrash, + hide: + access === 'read' || + getPublicLinkToken() || + inTrash || + !FeatureTogglesService.isActiveFeatureName(FeatureNames.COMPANY_MANAGE_ACCESS), onClick: () => setAccessModalState({ open: true, id: item.id }), }, { type: 'separator', hide: inTrash }, diff --git a/tdrive/frontend/src/app/views/client/body/drive/modals/update-access/index.tsx b/tdrive/frontend/src/app/views/client/body/drive/modals/update-access/index.tsx index bb816a0dc..29ee025a4 100644 --- a/tdrive/frontend/src/app/views/client/body/drive/modals/update-access/index.tsx +++ b/tdrive/frontend/src/app/views/client/body/drive/modals/update-access/index.tsx @@ -58,7 +58,7 @@ const AccessModalContent = (props: { } >
- {FeatureTogglesService.isActiveFeatureName(FeatureNames.COMPANY_SEARCH_USERS) && ( + {FeatureTogglesService.isActiveFeatureName(FeatureNames.COMPANY_MANAGE_ACCESS) && ( )}
diff --git a/tdrive/frontend/src/app/views/client/side-bar/index.tsx b/tdrive/frontend/src/app/views/client/side-bar/index.tsx index f9e0b3f68..c333e4ccc 100644 --- a/tdrive/frontend/src/app/views/client/side-bar/index.tsx +++ b/tdrive/frontend/src/app/views/client/side-bar/index.tsx @@ -111,7 +111,7 @@ export default () => { {Languages.t('components.side_menu.home')} )} - {FeatureTogglesService.isActiveFeatureName(FeatureNames.COMPANY_SEARCH_USERS) && ( + {FeatureTogglesService.isActiveFeatureName(FeatureNames.COMPANY_MANAGE_ACCESS) && (