Пользовательская функция Firestore.rules не оценивается должным образом - PullRequest
0 голосов
/ 11 ноября 2019

У меня есть следующая иерархия документов: / organization / {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;
}

Это не похоже на работу. Хотя он правильно позволяет администраторам читать / создавать / редактировать, он не позволяет читать аутентифицированному пользователю с тем же запросом request.auth.uid, что и значением teacherUid, хранящимся в документе родительского класса.

Я проверял этос помощью онлайн-симулятора консолей Firebase, а также запуска модульных тестов Mocha с помощью локального эмулятора FireStore.

Кажется, я не могу понять, в чем проблема.

Вот соответствующие части моего документа 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.

1 Ответ

0 голосов
/ 11 ноября 2019

Тьфу. :) Нашел проблему. Публикация моего ответа здесь на случай, если это кого-то запутает

Согласно документации по пользовательской функции Firestore функция должна быть объявлена ​​в области действия переменных, которые она использует, или переменные могут быть переданы в качестве аргументов.

По аналогии ста же проблема для SO здесь: Перемещение правила безопасности Firestore к правилу прерывания пользовательской функции

Любой из двух следующих вариантов работает:

Опция 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. ХотяМне не нравится объявлять функции в моем коде, передавая все аргументы, также довольно уродливо, и функция даже не будет вызываться вне этой области, поэтому было более разумно объявить ее там.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...