back: delete user data administrative endpoint (#799)

This commit is contained in:
Eric Doughty-Papassideris
2025-03-09 23:36:58 +01:00
committed by Anton Shepilov
parent 9d35fbe0d7
commit cb61d4d8d1
21 changed files with 485 additions and 258 deletions
@@ -68,5 +68,11 @@ export interface ConsoleServiceClient {
backChannelLogout(logoutToken: string): Promise<void>;
/**
* Similar to backChannelLogout, but must be called by administrative authorized
* code in case of user immediate kick out
*/
userWasDeletedForceLogout(userId: string): Promise<void>;
resendVerificationEmail(email: string): Promise<void>;
}
@@ -115,6 +115,11 @@ export class ConsoleInternalClient implements ConsoleServiceClient {
throw new Error("Method should not be implemented.");
}
async userWasDeletedForceLogout(_userId: string) {
logger.info("Internal: userWasDeletedForceLogout");
throw new Error("Method should not be implemented.");
}
async verifyJwtSid(_sid: string): Promise<void> {
logger.info("Internal: verifyJwtSid");
}
@@ -199,7 +199,7 @@ export class ConsoleRemoteClient implements ConsoleServiceClient {
if (!user) {
throw CrudException.notFound(`User ${consoleUserId} doesn't exists`);
}
await gr.services.companies.removeUserFromCompany({ id: company.id }, { id: user.id });
await gr.services.companies.removeUserFromCompany({ id: company.id }, user.id);
}
async removeUser(consoleUserId: string): Promise<void> {
@@ -328,6 +328,16 @@ export class ConsoleRemoteClient implements ConsoleServiceClient {
}
}
async userWasDeletedForceLogout(userId: string) {
const sessionRepository = gr.services.console.getSessionRepo();
if (!sessionRepository) return;
const sessions = (await sessionRepository.find({ sub: userId })).getEntities();
for (const session of sessions) {
session.revoked_at = new Date().getTime();
await sessionRepository.save(session);
}
}
async verifyJwtSid(sid: string): Promise<void> {
const sessionRepository = gr.services.console.getSessionRepo();
if (sid) {
@@ -478,10 +478,17 @@ export class FileServiceImpl {
return this.algorithm;
}
}
/** Get the storage path prefix specific to a user of a company */
export const getUserPath = (user_id: string, company_id: string): string => {
return `${gr.platformServices.storage.getHomeDir()}/files/${company_id}/${
user_id || "anonymous"
}/`;
};
/** Get the storage path prefix specific to a given File of a user of a company */
export const getFilePath = (
entity: File | { company_id: string; user_id?: string; id: string },
): string => {
return `${gr.platformServices.storage.getHomeDir()}/files/${entity.company_id}/${
entity.user_id || "anonymous"
}/${entity.id}`;
return `${getUserPath(entity.user_id, entity.company_id)}${entity.id}`;
};
@@ -13,7 +13,7 @@ import {
import Repository, {
FindOptions,
} from "../../../core/platform/services/database/services/orm/repository/repository";
import { UserPrimaryKey } from "../entities/user";
import User, { UserPrimaryKey } from "../entities/user";
import Company, {
CompanyPrimaryKey,
CompanySearchKey,
@@ -178,13 +178,13 @@ export class CompanyServiceImpl {
async removeUserFromCompany(
companyPk: CompanyPrimaryKey,
userPk: UserPrimaryKey,
userId: string,
context?: ExecutionContext,
): Promise<DeleteResult<CompanyUser>> {
const entity = await this.companyUserRepository.findOne(
{
group_id: companyPk.id,
user_id: userPk.id,
user_id: userId,
},
{},
context,
@@ -192,7 +192,7 @@ export class CompanyServiceImpl {
if (entity) {
await Promise.all([this.companyUserRepository.remove(entity, context)]);
const user = await gr.services.users.get(userPk);
const user = await gr.services.users.get({ id: userId });
if ((user.cache?.companies || []).includes(companyPk.id)) {
// Update user cache with companies
user.cache.companies = user.cache.companies.filter(id => id != companyPk.id);
@@ -304,14 +304,17 @@ export class CompanyServiceImpl {
}
async ensureDeletedUserNotInCompanies(userPk: UserPrimaryKey): Promise<void> {
const user = await gr.services.users.get(userPk);
if (user.deleted) {
const companies = await this.getAllForUser(user.id);
const user = await gr.services.users.get({ id: userPk.id });
if (user?.deleted ?? (userPk as User).deleted) {
const companies = await this.getAllForUser(user?.id ?? userPk.id);
for (const company of companies) {
logger.warn(`User ${userPk.id} is deleted so removed from company ${company.id}`);
await this.removeUserFromCompany(company, user);
await this.removeUserFromCompany(company, user?.id ?? userPk.id);
try {
await gr.services.workspaces.ensureUserNotInCompanyIsNotInWorkspace(userPk, company.id);
await gr.services.workspaces.ensureUserNotInCompanyIsNotInWorkspace(
user?.id ?? userPk.id,
company.id,
);
} catch (err) {
logger.error({ err }, "Error removing user from company from workspace");
}
@@ -14,7 +14,7 @@ import Repository, {
FindFilter,
FindOptions,
} from "../../../../core/platform/services/database/services/orm/repository/repository";
import User, { UserPrimaryKey } from "../../entities/user";
import User, { getInstance as getUserInstance, UserPrimaryKey } from "../../entities/user";
import { ListUserOptions, SearchUserOptions } from "./types";
import CompanyUser from "../../entities/company_user";
import SearchRepository from "../../../../core/platform/services/search/repository";
@@ -154,32 +154,38 @@ export class UserServiceImpl {
return new DeleteResult<User>("user", instance, !!instance);
}
async anonymizeAndDelete(pk: UserPrimaryKey, context?: ExecutionContext) {
/** If `deleteData` is false, then the user is only marked deleted and no data is actually deleted */
async anonymizeAndDelete(pk: UserPrimaryKey, context?: ExecutionContext, deleteData?: boolean) {
const user = await this.get(pk);
if (context.user.server_request || context.user.id === user.id) {
const userCopy = { ...user } as User;
//We keep a part of the user id as new name
const partialId = user.id.toString().split("-")[0];
const userCopy = getUserInstance(user);
user.username_canonical = `deleted-user-${partialId}`;
user.email_canonical = `${partialId}@tdrive.removed`;
user.first_name = "";
user.last_name = "";
user.phone = "";
user.picture = "";
user.thumbnail_id = null;
user.status_icon = null;
user.deleted = true;
user.delete_process_started_epoch = new Date().getTime();
if (!user.deleted) {
//We keep a part of the user id as new name
const partialId = user.id.toString().split("-")[0];
await this.save(user);
user.username_canonical = `deleted-user-${partialId}`;
user.email_canonical = `${partialId}@tdrive.removed`;
user.first_name = "";
user.last_name = "";
user.phone = "";
user.picture = "";
user.thumbnail_id = null;
user.status_icon = null;
user.deleted = true;
user.delete_process_started_epoch = new Date().getTime();
await gr.services.console.getClient().userWasDeletedForceLogout(user.id);
await this.save(user);
}
localEventBus.publish<ResourceEventsPayload>("user:deleted", {
user: user,
});
await gr.platformServices.admin.deleteUser(userCopy);
return deleteData && (await gr.platformServices.admin.deleteUser(userCopy));
}
}
@@ -110,7 +110,10 @@ export class WorkspaceServiceImpl implements TdriveServiceProvider, Initializabl
//If user deleted from a company, remove it from all workspace
localEventBus.subscribe<ResourceEventsPayload>("company:user:deleted", async data => {
if (data?.user?.id && data?.company?.id)
gr.services.workspaces.ensureUserNotInCompanyIsNotInWorkspace(data.user, data.company.id);
gr.services.workspaces.ensureUserNotInCompanyIsNotInWorkspace(
data.user.id,
data.company.id,
);
});
return this;
@@ -717,7 +720,7 @@ export class WorkspaceServiceImpl implements TdriveServiceProvider, Initializabl
}
async ensureUserNotInCompanyIsNotInWorkspace(
userPk: UserPrimaryKey,
userId: string,
companyId: string,
context?: ExecutionContext,
): Promise<void> {
@@ -725,15 +728,15 @@ export class WorkspaceServiceImpl implements TdriveServiceProvider, Initializabl
for (const workspace of workspaces) {
const companyUser = await gr.services.companies.getCompanyUser(
{ id: workspace.company_id },
userPk,
{ id: userId },
context,
);
if (!companyUser) {
logger.warn(
`User ${userPk.id} is not in company ${workspace.company_id} so removing from workspace ${workspace.id}`,
`User ${userId} is not in company ${workspace.company_id} so removing from workspace ${workspace.id}`,
);
await this.removeUser(
{ workspaceId: workspace.id, userId: userPk.id },
{ workspaceId: workspace.id, userId: userId },
companyId,
context,
).then(() => null);