Skip to content

Commit 5df9074

Browse files
committed
Fix comment auditing and upload rules
Recognize Firebase UIDs separately from system actors and audit callable comment creations. Limit Storage validation to one reservation lookup and add the composite comments index.
1 parent 4fc0289 commit 5df9074

3 files changed

Lines changed: 42 additions & 6 deletions

File tree

firestore.indexes.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,15 @@
2828
{ "fieldPath": "createdAt", "order": "DESCENDING" }
2929
]
3030
},
31+
{
32+
"collectionGroup": "comments",
33+
"queryScope": "COLLECTION",
34+
"fields": [
35+
{ "fieldPath": "articleId", "order": "ASCENDING" },
36+
{ "fieldPath": "status", "order": "ASCENDING" },
37+
{ "fieldPath": "pinned", "order": "ASCENDING" }
38+
]
39+
},
3140
{
3241
"collectionGroup": "commentReplies",
3342
"queryScope": "COLLECTION",

functions/src/index.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1063,11 +1063,17 @@ function stripUndefinedDeep(value: unknown): unknown {
10631063
return value;
10641064
}
10651065

1066+
function isFirebaseUserUid(actorUid: string) {
1067+
// Firestore auth context has no "user" type. Client SDK writes often
1068+
// arrive as api_key/unknown with the Firebase UID in authId.
1069+
return Boolean(actorUid) && !actorUid.includes("@");
1070+
}
1071+
10661072
function isSystemAuditActor(actorUid: string, authType?: string) {
1073+
if (isFirebaseUserUid(actorUid)) return false;
10671074
return (
10681075
authType === "service_account" ||
10691076
authType === "system" ||
1070-
authType === "api_key" ||
10711077
/gserviceaccount\.com$/i.test(actorUid)
10721078
);
10731079
}
@@ -1203,6 +1209,9 @@ async function auditAuthenticatedClientWrite(params: {
12031209
after: admin.firestore.DocumentData | undefined;
12041210
}) {
12051211
if (!params.actorUid) return;
1212+
// Admin SDK / scheduled jobs have no signed-in user here. Callables that
1213+
// act for a person log that person themselves via writeServerAuditLog.
1214+
if (isSystemAuditActor(params.actorUid, params.authType)) return;
12061215
const operation = !params.before ? "create" : !params.after ? "delete" : "update";
12071216
const snapshot = params.after || params.before;
12081217
const actorHint =
@@ -5056,6 +5065,13 @@ export const createComment = onCall(
50565065
}));
50575066
if (reservationRef) transaction.delete(reservationRef);
50585067
});
5068+
await writeServerAuditLog({
5069+
actorUid: uid,
5070+
action: "comment.create",
5071+
category: "comments",
5072+
details: `create comment record ${commentRef.id}`,
5073+
targetId: commentRef.id,
5074+
});
50595075
return { success: true, commentId: commentRef.id };
50605076
}
50615077
);
@@ -5150,6 +5166,13 @@ export const createCommentReply = onCall(
51505166
}));
51515167
if (reservationRef) transaction.delete(reservationRef);
51525168
});
5169+
await writeServerAuditLog({
5170+
actorUid: uid,
5171+
action: "comment_reply.create",
5172+
category: "comments",
5173+
details: `create comment_reply record ${replyRef.id}`,
5174+
targetId: replyRef.id,
5175+
});
51535176
return { success: true, replyId: replyRef.id };
51545177
}
51555178
);

storage.rules

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -96,16 +96,20 @@ service firebase.storage {
9696
}
9797

9898
match /comments/{userId}/{reservationId}/{fileName} {
99+
function reservation() {
100+
return firestore.get(/databases/(default)/documents/commentUploadReservations/$(reservationId)).data;
101+
}
99102
allow get: if true;
103+
// Storage rules may read at most two Firestore documents. Account checks
104+
// stay on reserveCommentUploads / createComment so this rule only reads
105+
// the reservation.
100106
allow create: if request.auth != null &&
101107
request.auth.uid == userId &&
102-
(isActiveReaderAccount() || isStaffUser()) &&
103108
reservationId.matches('[a-zA-Z0-9_-]{20,64}') &&
104109
fileName.matches('[0-3]\\.(webp|jpg)') &&
105-
firestore.exists(/databases/(default)/documents/commentUploadReservations/$(reservationId)) &&
106-
firestore.get(/databases/(default)/documents/commentUploadReservations/$(reservationId)).data.uid == userId &&
107-
firestore.get(/databases/(default)/documents/commentUploadReservations/$(reservationId)).data.expiresAt >= request.time &&
108-
fileName in firestore.get(/databases/(default)/documents/commentUploadReservations/$(reservationId)).data.fileNames &&
110+
reservation().uid == userId &&
111+
reservation().expiresAt >= request.time &&
112+
fileName in reservation().fileNames &&
109113
request.resource.size <= 2 * 1024 * 1024 &&
110114
request.resource.contentType.matches('image/(webp|jpeg)');
111115
// Attachment objects are content-addressed by a random name. Edits must

0 commit comments

Comments
 (0)