Firestore安全规则文档ID通配符触发空值错误 列表查询失败
Firestore安全规则集合通配符触发列表查询空值错误修复
问题现象
根级存在study集合,需求为仅允许读取status字段值为"published"的文档。
- 单文档读取请求可正常生效
- 携带
status == "published"过滤条件的集合级查询返回错误:Null value error. for 'list'
原安全规则配置
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { ... function isStudyPublished(studyId) { return get(/databases/$(database)/documents/study/$(studyId)).data.status == "published"; } ... // Match for participant permissioning match /study/{studyId}/{document=**} { // Deny all requests to create, update, or delete the study allow create, update, delete: if false // Allow the requestor to read the study if it is published allow read: if isStudyPublished(studyId) } ... } }
复现问题的查询代码
正常生效的单文档查询
// successfully retrieves a single document export const getStudy: GetStudy = (studyId) => { return new Promise((resolve, reject) => { getDoc(doc(firestore, COLLECTION_KEYS.STUDY, studyId)) .then((doc) => { resolve(dataToStudy(doc.id, doc.data())); }) .catch((error) => reject(error)); }); };
报错的集合查询
// fails to retrieve multiple documents export const getStudies: GetStudies = (cursor) => { const studies: StudyDoc[] = []; return new Promise((resolve, reject) => { let q = query(collection(firestore, COLLECTION_KEYS.STUDY), where("status", "==", "published"), orderBy(FIELD_KEYS.UPDATED)); if (cursor) q = query(q, startAfter(cursor)); getDocs(q) .then((snapshot) => { snapshot.forEach((doc) => { studies.push(dataToStudy(doc.id, doc.data())); }); return resolve([studies, snapshot.docs[snapshot.docs.length - 1]]); }) .catch((error) => { return reject(error); }); }); };
排查过程
- 初始怀疑查询携带了不符合规则的请求条件,核对查询过滤逻辑后排除该可能
- 用debug函数埋点测试规则逻辑,发现只要引用
studyId变量就会校验失败,测试用规则如下:
match /study/{studyId}/{document=**} { // Deny all requests to create, update, or delete the study allow create, update, delete: if false // Allow the requestor to read the study if it is published allow read: if debug(debug(true) && (debug(studyId) != null)); }
- 查看
firestore-debug.log日志:- 单文档请求时
studyId可正常捕获到文档ID值:
bool_value: true string_value: "rMxuVTJ0O15qPjMbpEU1" bool_value: true- 集合查询时日志仅输出第一个布尔值,
studyId为null:
bool_value: true - 单文档请求时
问题根因
- 匹配路径写法错误:
match /study/{studyId}/{document=**}的路径结构要求study集合下必须先存在一层文档ID段,才能匹配后续路径。当发起/study根集合的列表查询时,请求路径没有绑定具体的文档ID,studyId通配符值为null,传入isStudyPublished函数后,get()尝试读取不存在的/study/$(null)路径,触发空值错误。 - 根文档判断逻辑冗余:读取
study集合根级文档时,本可以直接通过resource.data访问当前文档的字段,原规则额外调用get()拉取当前文档数据,不仅产生多余读取开销,还导致集合查询时规则引擎无法将查询条件与规则做一致性校验。
修复方案
拆分匹配规则,分别处理study根集合文档和其子集合/子文档的权限逻辑:
- 针对
study根集合下的文档,直接判断当前文档的status字段,无需调用get(),规则条件与查询的where过滤条件完全对齐,保证集合查询可正常通过校验 - 针对
study下的所有嵌套子集合/子文档,保留原有逻辑,通过捕获的studyId判断父级study文档是否已发布
修正后的规则示例:
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { function isStudyPublished(studyId) { return get(/databases/$(database)/documents/study/$(studyId)).data.status == "published"; } // 匹配study根集合下的文档 match /study/{studyId} { allow create, update, delete: if false; // 直接判断当前文档的status字段,和查询条件对齐 allow read: if resource.data.status == "published"; } // 匹配study下所有嵌套子集合/子文档 match /study/{studyId}/{document=**} { allow create, update, delete: if false; allow read: if isStudyPublished(studyId); } } }
内容的提问来源于stack exchange,提问作者Bernard
相关产品推荐
相关产品推荐

