chore/port-solidtime-audit-fixes #1

Merged
danijel merged 54 commits from chore/port-solidtime-audit-fixes into main 2026-07-12 21:13:09 +00:00
175 changed files with 5150 additions and 1164 deletions
Showing only changes of commit 7b0c2ea590 - Show all commits

View File

@@ -16,6 +16,31 @@ export const users: CollectionConfig = {
}, },
}, },
subject: { kind: "self", field: "id" }, subject: { kind: "self", field: "id" },
// Collection-level PII map consumed by the DSR walkers (audit finding
// A5): export includes fields marked exportable; the soft-delete path
// redacts them. `email` is auto-added by Payload's `auth: true` and has
// no explicit field entry below, so it MUST be declared here or Art. 15
// export misses it and Art. 17 soft delete leaves it behind.
pii: {
email: {
category: "contact-email",
purpose: ["account-authentication", "transactional-notifications"],
exportable: true,
restrictable: true,
},
username: {
category: "identification-username",
purpose: ["service-delivery"],
exportable: true,
restrictable: true,
},
displayName: {
category: "identification-username",
purpose: ["service-delivery"],
exportable: true,
restrictable: true,
},
},
}, },
fields: [ fields: [
{ {

View File

@@ -192,6 +192,57 @@ describe("PayloadDataDelete", () => {
expect(updateData).not.toHaveProperty("processingRestrictedAt"); expect(updateData).not.toHaveProperty("processingRestrictedAt");
}); });
it("redacts the auth-injected email field for a users-shaped collection (A5)", async () => {
const config = makeMockConfig([
{
slug: "users",
custom: {
subject: { field: "id", kind: "self" },
pii: {
email: {
category: "contact-email",
purpose: ["account-authentication"],
exportable: true,
restrictable: true,
},
username: {
category: "identification-username",
purpose: ["service-delivery"],
exportable: true,
restrictable: true,
},
displayName: {
category: "identification-username",
purpose: ["service-delivery"],
exportable: true,
restrictable: true,
},
},
},
},
]);
mockPayload.find.mockResolvedValue({
docs: [{ id: "alice", email: "alice@example.com", username: "alice" }],
});
const deleter = new PayloadDataDelete(config, auditLog, mockGetPayload);
const cert = await deleter.deleteSubjectData("alice", "soft");
expect(mockPayload.update).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
email: null,
username: null,
displayName: null,
}),
}),
);
expect(cert.affected[0]?.fields).toEqual(
expect.arrayContaining(["email", "username", "displayName"]),
);
});
it("owner role: does NOT set processingRestrictedAt", async () => { it("owner role: does NOT set processingRestrictedAt", async () => {
const config = makeMockConfig([ const config = makeMockConfig([
{ {

View File

@@ -72,6 +72,60 @@ describe("PayloadDataExport", () => {
expect(bundle.data["users"]?.asReference).toBeUndefined(); expect(bundle.data["users"]?.asReference).toBeUndefined();
}); });
it("exports the auth-injected email field for a users-shaped collection (A5)", async () => {
// Mirrors packages/auth users collection: email is auto-added by Payload
// `auth: true` and declared only in the collection-level custom.pii map.
const config = makeMockConfig([
{
slug: "users",
custom: {
subject: { field: "id", kind: "self" },
pii: {
email: {
category: "contact-email",
purpose: ["account-authentication"],
exportable: true,
restrictable: true,
},
username: {
category: "identification-username",
purpose: ["service-delivery"],
exportable: true,
restrictable: true,
},
displayName: {
category: "identification-username",
purpose: ["service-delivery"],
exportable: true,
restrictable: true,
},
},
},
},
]);
mockPayload.find.mockResolvedValue({
docs: [
{
id: "alice",
email: "alice@example.com",
username: "alice",
displayName: "Alice",
passwordHash: "secret-hash",
},
],
});
const exporter = new PayloadDataExport(config, auditLog, mockGetPayload);
const bundle = await exporter.exportSubjectData("alice", "json");
const row = bundle.data["users"]!.asSelf![0]!;
expect(row["email"]).toBe("alice@example.com");
expect(row["username"]).toBe("alice");
expect(row["displayName"]).toBe("Alice");
expect(row).not.toHaveProperty("passwordHash");
});
it("happy path — owner role: includes exportable PII fields", async () => { it("happy path — owner role: includes exportable PII fields", async () => {
const config = makeMockConfig([ const config = makeMockConfig([
{ {