✨ back: delete user data administrative endpoint (#799)
This commit is contained in:
committed by
Anton Shepilov
parent
9d35fbe0d7
commit
cb61d4d8d1
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user