Block GDPR delete through related holds
This commit is contained in:
1 parent
e78e9a8aa0
commit
5b68c629c4
2 files changed
+154
-4
No files matched your search
@@ -186,14 +186,80 @@ export class ComplianceService {
|
||||
const legalHold = this.db.prepare(`
|
||||
select id from legal_holds
|
||||
where org_id = ? and active = 1
|
||||
and ((entity_type = ? and entity_id = ?) or (entity_type = 'organization' and entity_id = ?))
|
||||
and (
|
||||
(entity_type = ? and entity_id = ?)
|
||||
or (entity_type = 'organization' and entity_id = ?)
|
||||
or (
|
||||
entity_type = 'chat_thread'
|
||||
and exists (
|
||||
select 1 from chat_messages
|
||||
where org_id = ?
|
||||
and thread_id = legal_holds.entity_id
|
||||
and sender_type = ?
|
||||
and sender_id = ?
|
||||
)
|
||||
)
|
||||
or (
|
||||
entity_type = 'file_object'
|
||||
and exists (
|
||||
select 1 from file_objects
|
||||
where org_id = ?
|
||||
and id = legal_holds.entity_id
|
||||
and owner_type = ?
|
||||
and owner_id = ?
|
||||
)
|
||||
)
|
||||
)
|
||||
limit 1
|
||||
`).get(input.orgId, input.subjectType, input.subjectId, input.orgId);
|
||||
`).get(
|
||||
input.orgId,
|
||||
input.subjectType,
|
||||
input.subjectId,
|
||||
input.orgId,
|
||||
input.orgId,
|
||||
input.subjectType,
|
||||
input.subjectId,
|
||||
input.orgId,
|
||||
input.subjectType,
|
||||
input.subjectId,
|
||||
);
|
||||
const retentionFreeze = this.db.prepare(`
|
||||
select id from retention_freezes
|
||||
where org_id = ? and active = 1 and scope_type = 'organization' and scope_id = ?
|
||||
where org_id = ? and active = 1
|
||||
and (
|
||||
(scope_type = 'organization' and scope_id = ?)
|
||||
or (
|
||||
scope_type = 'chat_space'
|
||||
and exists (
|
||||
select 1 from chat_messages
|
||||
where org_id = ?
|
||||
and space_id = retention_freezes.scope_id
|
||||
and sender_type = ?
|
||||
and sender_id = ?
|
||||
)
|
||||
)
|
||||
or (
|
||||
scope_type = 'file_object'
|
||||
and exists (
|
||||
select 1 from file_objects
|
||||
where org_id = ?
|
||||
and id = retention_freezes.scope_id
|
||||
and owner_type = ?
|
||||
and owner_id = ?
|
||||
)
|
||||
)
|
||||
)
|
||||
limit 1
|
||||
`).get(input.orgId, input.orgId);
|
||||
`).get(
|
||||
input.orgId,
|
||||
input.orgId,
|
||||
input.orgId,
|
||||
input.subjectType,
|
||||
input.subjectId,
|
||||
input.orgId,
|
||||
input.subjectType,
|
||||
input.subjectId,
|
||||
);
|
||||
if (legalHold || retentionFreeze) throw new Error("Active compliance hold prevents GDPR delete.");
|
||||
}
|
||||
|
||||
|
||||
@@ -162,4 +162,88 @@ describe("compliance service", () => {
|
||||
}))).toThrow("Active compliance hold prevents GDPR delete.");
|
||||
connection.close();
|
||||
});
|
||||
|
||||
test("blocks GDPR delete under related chat and file holds", () => {
|
||||
const connection = createConnection();
|
||||
const organization = new OrganizationService(connection.db);
|
||||
const chat = new ChatService(connection.db);
|
||||
const compliance = new ComplianceService(connection.db);
|
||||
let orgId = "";
|
||||
let employeeId = "";
|
||||
let spaceId = "";
|
||||
let threadId = "";
|
||||
let fileId = "";
|
||||
let holdId = "";
|
||||
let freezeId = "";
|
||||
|
||||
connection.transaction(() => {
|
||||
orgId = organization.createOrganization({
|
||||
organizationName: "Acme Operations",
|
||||
employeeName: "Dana Lee",
|
||||
mission: "Coordinate agents with auditable approvals.",
|
||||
goal: "Ship a verified control plane.",
|
||||
});
|
||||
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: "Held chat", attachmentFileIds: [] });
|
||||
threadId = (connection.db.prepare("select thread_id from chat_messages where org_id = ?").get(orgId) as { thread_id: string }).thread_id;
|
||||
fileId = "file-held";
|
||||
connection.db.prepare(`
|
||||
insert into file_objects(id, org_id, name, kind, size_bytes, sha256, storage_path, owner_type, owner_id, created_at)
|
||||
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(fileId, orgId, "held.txt", "text/plain", 4, "a".repeat(64), "artifacts/held.txt", "employee", employeeId, new Date().toISOString());
|
||||
holdId = compliance.createLegalHold({
|
||||
orgId,
|
||||
entityType: "chat_thread",
|
||||
entityId: threadId,
|
||||
reason: "Thread discovery",
|
||||
createdByEmployeeId: employeeId,
|
||||
});
|
||||
});
|
||||
|
||||
expect(() => connection.transaction(() => compliance.createExport({
|
||||
orgId,
|
||||
subjectType: "employee",
|
||||
subjectId: employeeId,
|
||||
exportType: "gdpr_delete",
|
||||
requestedByEmployeeId: employeeId,
|
||||
}))).toThrow("Active compliance hold prevents GDPR delete.");
|
||||
|
||||
connection.transaction(() => {
|
||||
compliance.setLegalHoldState({ orgId, legalHoldId: holdId, active: false, employeeId });
|
||||
freezeId = compliance.createRetentionFreeze({
|
||||
orgId,
|
||||
scopeType: "chat_space",
|
||||
scopeId: spaceId,
|
||||
reason: "Space retention",
|
||||
createdByEmployeeId: employeeId,
|
||||
});
|
||||
});
|
||||
expect(() => connection.transaction(() => compliance.createExport({
|
||||
orgId,
|
||||
subjectType: "employee",
|
||||
subjectId: employeeId,
|
||||
exportType: "gdpr_delete",
|
||||
requestedByEmployeeId: employeeId,
|
||||
}))).toThrow("Active compliance hold prevents GDPR delete.");
|
||||
|
||||
connection.transaction(() => {
|
||||
compliance.setRetentionFreezeState({ orgId, retentionFreezeId: freezeId, active: false, employeeId });
|
||||
compliance.createLegalHold({
|
||||
orgId,
|
||||
entityType: "file_object",
|
||||
entityId: fileId,
|
||||
reason: "File discovery",
|
||||
createdByEmployeeId: employeeId,
|
||||
});
|
||||
});
|
||||
expect(() => connection.transaction(() => compliance.createExport({
|
||||
orgId,
|
||||
subjectType: "employee",
|
||||
subjectId: employeeId,
|
||||
exportType: "gdpr_delete",
|
||||
requestedByEmployeeId: employeeId,
|
||||
}))).toThrow("Active compliance hold prevents GDPR delete.");
|
||||
connection.close();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user