Redact employee references during GDPR delete

This commit is contained in:
ZacharyZhang-NY committed 2026-05-23 22:38:31 -04:00
1 parent 689fe861f1
commit ddd3b09bd1
2 files changed
+75 -1

No files matched your search

@@ -26,6 +26,7 @@ export class ComplianceService {
if (input.exportType === COMPLIANCE_EXPORT_TYPE.GDPR_DELETE) this.assertDeleteAllowed(input);
const payload = this.subjectPayload(input.orgId, input.subjectType, input.subjectId);
if (input.exportType === COMPLIANCE_EXPORT_TYPE.GDPR_DELETE) this.applyGdprDelete(input.orgId, input.subjectType, input.subjectId);
const requestedByEmployeeId = deletedEmployeeRequestor(input) ? null : input.requestedByEmployeeId;
const id = createId("cmp");
const now = new Date().toISOString();
this.db.prepare(`
@@ -41,7 +42,7 @@ export class ComplianceService {
input.exportType,
COMPLIANCE_EXPORT_STATE.COMPLETED,
JSON.stringify(payload),
input.requestedByEmployeeId,
requestedByEmployeeId,
now,
now,
);
@@ -152,6 +153,12 @@ export class ComplianceService {
chatMessages: this.db.prepare("select * from chat_messages where org_id = ? and sender_type = 'employee' and sender_id = ? order by created_at").all(orgId, subjectId),
approvalsRequested: this.db.prepare("select * from approvals where org_id = ? and requested_by_type = 'employee' and requested_by_id = ? order by created_at").all(orgId, subjectId),
approvalsDecided: this.db.prepare("select * from approvals where org_id = ? and decided_by_employee_id = ? order by created_at").all(orgId, subjectId),
employeeIdentities: this.db.prepare("select * from employee_identities where org_id = ? and employee_id = ? order by created_at").all(orgId, subjectId),
roleGrants: this.db.prepare("select * from role_grants where org_id = ? and principal_type = 'employee' and principal_id = ? order by created_at").all(orgId, subjectId),
socialAccountsAuthorized: this.db.prepare("select * from social_accounts where org_id = ? and authorized_employee_id = ? order by created_at").all(orgId, subjectId),
socialAccountGrants: this.db.prepare("select * from social_account_grants where org_id = ? and employee_id = ? order by created_at").all(orgId, subjectId),
scheduledPostsCreated: this.db.prepare("select * from scheduled_posts where org_id = ? and created_by_employee_id = ? order by created_at").all(orgId, subjectId),
complianceExportsRequested: this.db.prepare("select * from compliance_exports where org_id = ? and requested_by_employee_id = ? order by created_at").all(orgId, subjectId),
auditEventsAsActor: this.db.prepare("select * from audit_events where org_id = ? and actor_type = 'employee' and actor_id = ? order by created_at").all(orgId, subjectId),
};
}
@@ -172,6 +179,35 @@ export class ComplianceService {
.run(orgId, subjectId);
this.db.prepare("update approvals set requested_by_id = null where org_id = ? and requested_by_type = 'employee' and requested_by_id = ?")
.run(orgId, subjectId);
this.db.prepare("update approvals set decided_by_employee_id = null where org_id = ? and decided_by_employee_id = ?")
.run(orgId, subjectId);
this.db.prepare("delete from employee_identities where org_id = ? and employee_id = ?").run(orgId, subjectId);
this.db.prepare("delete from chat_read_state where org_id = ? and employee_id = ?").run(orgId, subjectId);
this.db.prepare("delete from social_account_grants where org_id = ? and employee_id = ?").run(orgId, subjectId);
this.db.prepare("update social_accounts set authorized_employee_id = null where org_id = ? and authorized_employee_id = ?")
.run(orgId, subjectId);
this.db.prepare("update social_approval_routes set route_to_id = null where org_id = ? and route_to_type = 'authorized_employee' and route_to_id = ?")
.run(orgId, subjectId);
this.db.prepare("update scheduled_posts set created_by_employee_id = null where org_id = ? and created_by_employee_id = ?")
.run(orgId, subjectId);
this.db.prepare("update compliance_exports set requested_by_employee_id = null where org_id = ? and requested_by_employee_id = ?")
.run(orgId, subjectId);
this.db.prepare("update legal_holds set created_by_employee_id = null where org_id = ? and created_by_employee_id = ?")
.run(orgId, subjectId);
this.db.prepare("update retention_freezes set created_by_employee_id = null where org_id = ? and created_by_employee_id = ?")
.run(orgId, subjectId);
this.db.prepare("update audit_replay_runs set created_by_employee_id = null where org_id = ? and created_by_employee_id = ?")
.run(orgId, subjectId);
this.db.prepare("update rollback_snapshots set created_by_employee_id = null where org_id = ? and created_by_employee_id = ?")
.run(orgId, subjectId);
this.db.prepare("update migration_reconciliation_items set resolved_by_employee_id = null where org_id = ? and resolved_by_employee_id = ?")
.run(orgId, subjectId);
this.db.prepare("delete from approval_assignments where org_id = ? and assignee_type = 'employee' and assignee_id = ?")
.run(orgId, subjectId);
this.db.prepare("delete from role_grants where org_id = ? and principal_type = 'employee' and principal_id = ?")
.run(orgId, subjectId);
this.db.prepare("update role_grants set granted_by_employee_id = null where org_id = ? and granted_by_employee_id = ?")
.run(orgId, subjectId);
return;
}
this.db.prepare("update agents set name = ?, role = ?, lifecycle = ?, model_provider_id = null, model_id = null where id = ? and org_id = ?")
@@ -309,3 +345,9 @@ export class ComplianceService {
if (!row) throw new Error(message);
}
}
function deletedEmployeeRequestor(input: CreateComplianceExportInput): boolean {
return input.exportType === COMPLIANCE_EXPORT_TYPE.GDPR_DELETE
&& input.subjectType === "employee"
&& input.subjectId === input.requestedByEmployeeId;
}
@@ -3,10 +3,13 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
import { DatabaseConnection } from "../../src/main/database/connection";
import { ApprovalService } from "../../src/main/services/approval-service";
import { ChatService } from "../../src/main/services/chat-service";
import { ComplianceService } from "../../src/main/services/compliance-service";
import { OrganizationService } from "../../src/main/services/organization-service";
import { SnapshotService } from "../../src/main/services/snapshot-service";
import { SocialService } from "../../src/main/services/social-service";
import { APPROVAL_STATE, SOCIAL_PLATFORM } from "../../src/shared/domain";
function createConnection(): DatabaseConnection {
return new DatabaseConnection(join(tmpdir(), `agentcompany-new-compliance-${randomUUID()}.db`));
@@ -64,13 +67,17 @@ describe("compliance service", () => {
test("blocks GDPR delete under holds and redacts after release", () => {
const connection = createConnection();
const organization = new OrganizationService(connection.db);
const approvals = new ApprovalService(connection.db);
const chat = new ChatService(connection.db);
const compliance = new ComplianceService(connection.db);
const social = new SocialService(connection.db);
const snapshot = new SnapshotService(connection.db);
let orgId = "";
let employeeId = "";
let spaceId = "";
let holdId = "";
let approvalId = "";
let socialAccountId = "";
connection.transaction(() => {
orgId = organization.createOrganization({
@@ -82,6 +89,24 @@ describe("compliance service", () => {
employeeId = (connection.db.prepare("select id from employees where org_id = ?").get(orgId) as { id: string }).id;
spaceId = chat.createSpace({ orgId, spaceType: "project", name: "release", topic: "Release coordination", visibility: "workspace" });
chat.sendMessage({ orgId, spaceId, threadId: null, senderType: "employee", senderId: employeeId, bodyMd: "Delete me", attachmentFileIds: [] });
approvalId = approvals.create({
orgId,
kind: "security_review",
title: "Security review",
details: "Employee requested and decided.",
risk: "low",
requestedByType: "employee",
requestedById: employeeId,
});
approvals.decide({ orgId, approvalId, state: APPROVAL_STATE.APPROVED, decidedByEmployeeId: employeeId });
socialAccountId = social.createAccount({
orgId,
platform: SOCIAL_PLATFORM.X,
handle: "@acme",
displayName: "Acme",
createdByEmployeeId: employeeId,
authorizedEmployeeId: employeeId,
});
holdId = compliance.createLegalHold({
orgId,
entityType: "employee",
@@ -111,9 +136,16 @@ describe("compliance service", () => {
});
const state = snapshot.load();
const redactedApproval = state.approvals.find((approval) => approval.id === approvalId);
const redactedSocialAccount = state.socialAccounts.find((account) => account.id === socialAccountId);
expect(state.employees[0]?.displayName).toBe("Deleted employee");
expect(state.chatMessages[0]?.senderId).toBeNull();
expect(redactedApproval?.requestedById).toBeNull();
expect(redactedApproval?.decidedByEmployeeId).toBeNull();
expect(redactedSocialAccount?.authorizedEmployeeId).toBeNull();
expect(state.socialAccountGrants).toHaveLength(0);
expect(state.complianceExports[0]?.exportType).toBe("gdpr_delete");
expect(state.complianceExports[0]?.requestedByEmployeeId).toBeNull();
expect(state.auditEvents.some((event) => event.entityType === "compliance_export" && event.action === "gdpr_deleted")).toBe(true);
connection.close();
});