diff --git a/tdrive/backend/node/src/services/console/clients/remote.ts b/tdrive/backend/node/src/services/console/clients/remote.ts index 5014413d..22c8ff2d 100644 --- a/tdrive/backend/node/src/services/console/clients/remote.ts +++ b/tdrive/backend/node/src/services/console/clients/remote.ts @@ -23,6 +23,32 @@ import { ConsoleServiceImpl } from "../service"; import coalesce from "../../../utils/coalesce"; import config from "config"; import { CompanyUserRole } from "src/services/user/web/types"; +import { v5 as uuidv5 } from "uuid"; + +// [Workavia] Multi-tenant company resolution. +// +// Upstream tdrive hardcodes every OIDC user into the all-zero Company +// UUID. That breaks Workavia's tenant isolation: users from different +// customer domains (e.g. meconsult.fr, acme.com) would all land in the +// same Company and could see each other's "Shared with me" results, +// search each other in the share-with-user picker, etc. +// +// We derive a deterministic Company UUID per email domain via uuidv5 +// over a fixed namespace, and use the bare domain as the Company's +// human name. First user from a domain bootstraps the Company; every +// subsequent user from that domain joins it. +// +// Namespace UUID is stable & arbitrary — picked once and committed. +const WORKAVIA_NAMESPACE = "1b1c8c44-3a5a-4e2c-bfa1-d3e2f1a0c0ff"; + +function workaviaCompanyForEmail(email: string): { id: string; name: string } { + // Default fallback for malformed emails — keeps behaviour identical + // to upstream (one shared Company) for the broken case so we don't + // break the system on a typo. + const at = email.lastIndexOf("@"); + const domain = at >= 0 ? email.slice(at + 1).toLowerCase() : "tdrive.local"; + return { id: uuidv5(domain, WORKAVIA_NAMESPACE), name: domain }; +} import Session from "../entities/session"; export class ConsoleRemoteClient implements ConsoleServiceClient { version: "1"; @@ -166,15 +192,16 @@ export class ConsoleRemoteClient implements ConsoleServiceClient { await gr.services.users.save(user); - //For now TDrive works with only one company as we don't get it from the SSO - let company = await gr.services.companies.getCompany({ - id: "00000000-0000-4000-0000-000000000000", - }); + // [Workavia] Resolve the Company from the user's email domain so + // each tenant of a Workavia install gets its own isolated Company. + // Upstream behaviour was a single shared Company. + const tenant = workaviaCompanyForEmail(userDTO.email); + let company = await gr.services.companies.getCompany({ id: tenant.id }); if (!company) { company = await gr.services.companies.createCompany( getCompanyInstance({ - id: "00000000-0000-4000-0000-000000000000", - name: "Tdrive", + id: tenant.id, + name: tenant.name, plan: { name: "Local", limits: undefined, features: undefined }, }), ); diff --git a/tdrive/backend/node/src/services/console/web/controller.ts b/tdrive/backend/node/src/services/console/web/controller.ts index 703149f2..b0c1e7a1 100644 --- a/tdrive/backend/node/src/services/console/web/controller.ts +++ b/tdrive/backend/node/src/services/console/web/controller.ts @@ -23,6 +23,18 @@ import { getInstance as getCompanyInstance } from "../../../services/user/entiti import Workspace from "../../../services/workspaces/entities/workspace"; import gr from "../../global-resolver"; import { Configuration } from "../../../core/platform/framework"; +import { v5 as uuidv5 } from "uuid"; + +// [Workavia] Same multi-tenant logic as in console/clients/remote.ts — +// derive the Company from the email domain instead of grouping every +// user into a single Company. +const WORKAVIA_NAMESPACE = "1b1c8c44-3a5a-4e2c-bfa1-d3e2f1a0c0ff"; + +function workaviaCompanyForEmail(email: string): { id: string; name: string } { + const at = email.lastIndexOf("@"); + const domain = at >= 0 ? email.slice(at + 1).toLowerCase() : "tdrive.local"; + return { id: uuidv5(domain, WORKAVIA_NAMESPACE), name: domain }; +} import { CompanyUserRole } from "src/services/user/web/types"; import config from "config"; @@ -96,15 +108,19 @@ export class ConsoleController { const user = await gr.services.users.create(newUser); await gr.services.users.setPassword({ id: user.entity.id }, request.body.password); - //Create a global company for all users in local mode - const companies = await gr.services.companies.getCompanies(); - let company = companies.getEntities()?.[0]; + // [Workavia] Resolve the Company from the user's email domain + // (same logic as the OIDC/remote path). Each tenant gets its + // own isolated Company; first signup of a domain bootstraps it. + const tenant = workaviaCompanyForEmail(email); + let company = await gr.services.companies.getCompany({ id: tenant.id }); if (!company) { - const newCompany = getCompanyInstance({ - name: "Tdrive", - plan: { name: "Local", limits: undefined, features: undefined }, - }); - company = await gr.services.companies.createCompany(newCompany); + company = await gr.services.companies.createCompany( + getCompanyInstance({ + id: tenant.id, + name: tenant.name, + plan: { name: "Local", limits: undefined, features: undefined }, + }), + ); } if (this.rootAdmins.includes(email)) { userRole = "admin"; diff --git a/tdrive/frontend/public/index.html b/tdrive/frontend/public/index.html index cd2c63d1..85702f44 100644 --- a/tdrive/frontend/public/index.html +++ b/tdrive/frontend/public/index.html @@ -2,7 +2,7 @@
-