fix(auth): declare users email/username/displayName in DSR pii map
The DSR walkers read the COLLECTION-level custom.pii map, which the users collection never declared — Art. 15 export returned bare ids and Art. 17 soft delete redacted nothing; the auth-injected email field in particular was invisible (audit finding A5). Declares email (auto-added by Payload auth: true), username and displayName as exportable + restrictable; walker tests pin a users-shaped collection end to end. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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([
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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([
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user