【问题标题】:Firestore.rules custom function not evaluating as intendedFirestore.rules 自定义函数未按预期进行评估
【发布时间】:2019-11-11 06:15:58
【问题描述】:

我有以下文档层次结构: /organizations/{orgId}/classes/{classId}/students/{studentId}

想法是班级文档有一个teacherUid字段,该字段当前存储分配给班级的老师的Uid。只有教师或管理员才能在课堂上阅读/创建/编辑学生。 *请注意,我只是在测试老师的阅读,遇到了这个障碍,之后我将对创建/更新权限应用相同的规则。

我有以下 firestore.rules:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /organizations/{orgId} {
      allow read: if isAdmin();
      allow create, update: if isAdmin();

      match /classes/{classId} {
        allow read: if request.auth.uid != null;
        allow create, update: if isAdmin();

        match /students/{studentId} {
          allow read: if isAdmin() || belongsToCurrentClass();
          allow create, update: if isAdmin();
        }
      }
    }
  }
}


function isAdmin() {
  // Removed for security.  isAdmin routine currently works correctly
}

function belongsToCurrentClass() {
  // returns true if the authenticated user is the teacher of the requested class
  return get(/databases/$(database)/documents/organizations/$(orgId)/classes/$(classId)).data.teacherUid == request.auth.uid;
}

这似乎不起作用。虽然它正确地允许管理员读取/创建/编辑,但它不允许经过身份验证的用户使用与存储在父类文档中的teacherUid 值相同的 request.auth.uid 进行读取。

我已经使用在线 firebase 控制台 firestore 模拟器以及运行本地 firestore 模拟器的 mocha 单元测试对此进行了测试。

我似乎无法弄清楚问题所在。

以下是我的 test.js 文档的相关部分:

const fs = require('fs');
const path = require('path');

const TEST_FIREBASE_PROJECT_ID = 'test-firestore-rules-project';

const firebase = require('@firebase/testing');

const authTeacher = {
  uid: 'testTeacher1',
};

const authAdmin = {
  // Removed for security
};

before(async () => {
  // The above was from the codelab.  Commenting out the below since we aren't testing rules at this moment.
  const rulesContent = fs.readFileSync(path.resolve(__dirname, '../../firestore.rules'));
  await firebase.loadFirestoreRules({
    projectId: TEST_FIREBASE_PROJECT_ID,
    rules: rulesContent,
  });
});

after(() => {
  firebase.apps().forEach(app => app.delete());
});

...

describe('Classes/Students/* rules', () => {
  const testClassPath = 'organizations/testOrg/classes/testClass';
  const testStudentPath = testClassPath + '/students/testStudent';
  const newStudentPath = testClassPath + '/students/newStudent';
  const testOtherClassPath = 'organizations/testOrg/classes/testClass';
  const testOtherStudentPath = testOtherClassPath + '/students/testOtherStudent';
  const newOtherStudentPath = testOtherClassPath + '/students/newOtherStudent';

  const dbUnauth = firebase
    .initializeTestApp({
      projectId: TEST_FIREBASE_PROJECT_ID,
    })
    .firestore();

  const dbTeacher = firebase
    .initializeTestApp({
      projectId: TEST_FIREBASE_PROJECT_ID,
      auth: authTeacher,
    })
    .firestore();

  const dbAdmin = firebase
    .initializeTestApp({
      projectId: TEST_FIREBASE_PROJECT_ID,
      auth: authAdmin,
    })
    .firestore();

  before(async () => {
    const admin = firebase
      .initializeAdminApp({
        projectId: TEST_FIREBASE_PROJECT_ID,
      })
      .firestore();

    // Create Class - for testing classes that belong to the authenticated user
    await admin.doc(testClassPath).set({
      teacherUid: authTeacher.uid,
    });

    // Create Student
    await admin.doc(testStudentPath).set({
      name: 'John Smith',
    });

    // Create Other Class - for testing classes that belong to other users
    await admin.doc(testOtherClassPath).set({
      teacherUid: 'someOtherTeacherUid',
    });

    // Create Other Student
    await admin.doc(testOtherStudentPath).set({
      name: 'Cave Johnson',
    });
  });

  after(() => {
    // Clear data from the emulator
    firebase.clearFirestoreData({ projectId: TEST_FIREBASE_PROJECT_ID });
  });

  it('Unauthenticated users cannot access students', async () => {
    await firebase.assertFails(dbUnauth.doc(testStudentPath).get());
  });

  it('Unauthenticated users cannot create students', async () => {
    await firebase.assertFails(
      dbUnauth.doc(newStudentPath).set({
        name: 'Jane Doe',
      })
    );
  });

  it('Non-admin users can read students', async () => {
    await firebase.assertSucceeds(dbTeacher.doc(testStudentPath).get());
  });

  it('Non-admin users cannot read students from another user', async () => {
    await firebase.assertFails(dbTeacher.doc(testOtherStudentPath).get());
  });

  it('Non-admin users can edit students', async () => {
    await firebase.assertSucceeds(
      dbTeacher.doc(testStudentPath).set({
        anotherProperty: 'Some Value',
      })
    );
  });

  it('Non-admin users cannot edit students from another user', async () => {
    await firebase.assertFails(
      dbTeacher.doc(testOtherStudentPath).set({
        anotherProperty: 'Some Value',
      })
    );
  });

  it('Non-admin users can create students', async () => {
    await firebase.assertSucceeds(
      dbTeacher.doc(newStudentPath).set({
        name: 'Jane Doe',
      })
    );
  });

  it('Non-admin users cannot create students in a class they do not belong to', async () => {
    await firebase.assertFails(
      dbTeacher.doc(testOtherStudentPath).set({
        name: 'Jane Doe',
      })
    );
  });

  it('Non-admin users cannot delete students', async () => {
    await firebase.assertFails(dbTeacher.doc(testStudentPath).delete());
  });

  it('Admin users can read students', async () => {
    await firebase.assertSucceeds(dbAdmin.doc(testStudentPath).get());
  });

  it('Admin users can create students', async () => {
    await firebase.assertSucceeds(
      dbAdmin.doc(newStudentPath).set({
        name: 'Jane Doe',
      })
    );
  });

  it('Admin users can edit students', async () => {
    await firebase.assertSucceeds(
      dbAdmin.doc(testStudentPath).set({
        anotherProperty: 'Some Value',
      })
    );
  });

  it('Admin users cannot delete students', async () => {
    await firebase.assertFails(dbAdmin.doc(testStudentPath).delete());
  });
});

这是运行单元测试时的错误输出:

PS C:\Local\Personal\Angular Projects\TSI\functions> npm test

> functions@ test C:\Local\Personal\Angular Projects\TSI\functions
> mocha



  Organization rules
    √ Unauthenticated users cannot read organizations (48ms)
    √ Unauthenticated users cannot create orgs organizations
    √ Unauthenticated users cannot delete organizations
    √ Non-admin users cannot read organizations (45ms)
    √ Non-admin users cannot edit organizations
    √ Non-admin users cannot create organizations
    √ Non-admin users cannot delete organizations
    √ Admin users can read organizations (47ms)
    √ Admin users can create organizations
    √ Admin users can edit organizations
    √ Admin users cannot delete organizations

  Classes rules
    √ Unauthenticated users cannot access classes
    √ Unauthenticated users cannot create classes
    √ Unauthenticated users cannot delete classes
    √ Non-admin users can read classes (38ms)
    √ Non-admin users cannot edit classes
    √ Non-admin users cannot create classes
    √ Non-admin users cannot delete classes
    √ Admin users can read classes
    √ Admin users can create classes
    √ Admin users can edit classes
    √ Admin users cannot delete classes

  Classes/Students/* rules
    √ Unauthenticated users cannot access students
    √ Unauthenticated users cannot create students
    1) Non-admin users can read students
    √ Non-admin users cannot read students from another user
    2) Non-admin users can edit students
    √ Non-admin users cannot edit students from another user
    3) Non-admin users can create students
    √ Non-admin users cannot create students in a class they do not belong to
    √ Non-admin users cannot delete students
    √ Admin users can read students
    √ Admin users can create students
    √ Admin users can edit students
    √ Admin users cannot delete students


  32 passing (3s)
  3 failing

  1) Classes/Students/* rules
       Non-admin users can read students:
     FirebaseError: 
Null value error. for 'get' @ L15
      at new FirestoreError ...
  2) Classes/Students/* rules
       Non-admin users can edit students:
     FirebaseError: 7 PERMISSION_DENIED: 
false for 'update' @ L16
      at new FirestoreError ...
  3) Classes/Students/* rules
     FirebaseError: 7 PERMISSION_DENIED: 
false for 'create' @ L16
      at new FirestoreError ...

npm ERR! Test failed.  See above for more details.

【问题讨论】:

  • 仅供参考,如果我可以在调试会话中运行规则评估,我可能会在 10 秒内回答这个问题。如果您知道如何在规则引擎评估请求时检查值,我很乐意在这里。为了不在同一个 SO 帖子中问两个问题,我在这里询问了调试建议:stackoverflow.com/questions/58796514/…

标签: firebase google-cloud-firestore firebase-security


【解决方案1】:

呃。 :) 发现问题。在这里发布我的答案以防万一其他人绊倒。

根据Firestore custom function documentation,函数需要在它使用的变量范围内声明,或者变量可以作为参数传递。

与此处的 SO 上的相同问题类似: Moving Firestore security rule to custom function breaks rule

以下两个选项之一有效:

选项 1 - 在变量范围内声明:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /organizations/{orgId} {
      allow read: if isAdmin();
      allow create, update: if isAdmin();

      match /classes/{classId} {
        function belongsToCurrentClass() {
          // retuns true if the authenticated user is the teacher of the requested class
          return get(/databases/$(database)/documents/organizations/$(orgId)/classes/$(classId)).data.teacherUid == request.auth.uid;
        }

        allow read: if request.auth.uid != null;
        allow create, update: if isAdmin();

        match /students/{studentId} {
          allow read: if isAdmin() || belongsToCurrentClass();
          allow create, update: if isAdmin() || belongsToCurrentClass();
        }
      }
    }
  }
}

function isAdmin() {
  // Removed for security.
}

选项 2 - 将变量作为参数传递:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /organizations/{orgId} {
      allow read: if isAdmin();
      allow create, update: if isAdmin();

      match /classes/{classId} {
        allow read: if request.auth.uid != null;
        allow create, update: if isAdmin();

        match /students/{studentId} {
          allow read: if isAdmin() || belongsToCurrentClass();
          allow create, update: if isAdmin() || belongsToCurrentClass(database, orgId, classId);
        }
      }
    }
  }
}

function isAdmin() {
  // Removed for security.
}

function belongsToCurrentClass(database, orgId, classId) {
  // returns true if the authenticated user is the teacher of the requested class
  return get(/databases/$(database)/documents/organizations/$(orgId)/classes/$(classId)).data.teacherUid == request.auth.uid;
}

我个人选择了选项 1。虽然我不喜欢在我的代码中声明传递所有参数的函数也很丑陋,而且该函数甚至不会在该范围之外被调用,所以它更有意义在那里声明。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-11-25
    • 2014-04-15
    • 1970-01-01
    • 2020-12-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多