You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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
    

问题根因

  1. 匹配路径写法错误:match /study/{studyId}/{document=**}的路径结构要求study集合下必须先存在一层文档ID段,才能匹配后续路径。当发起/study根集合的列表查询时,请求路径没有绑定具体的文档ID,studyId通配符值为null,传入isStudyPublished函数后,get()尝试读取不存在的/study/$(null)路径,触发空值错误。
  2. 根文档判断逻辑冗余:读取study集合根级文档时,本可以直接通过resource.data访问当前文档的字段,原规则额外调用get()拉取当前文档数据,不仅产生多余读取开销,还导致集合查询时规则引擎无法将查询条件与规则做一致性校验。

修复方案

拆分匹配规则,分别处理study根集合文档和其子集合/子文档的权限逻辑:

  1. 针对study根集合下的文档,直接判断当前文档的status字段,无需调用get(),规则条件与查询的where过滤条件完全对齐,保证集合查询可正常通过校验
  2. 针对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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 00:27:19