3
votes

I'm currently writing some rules for my app with a Firestore database. Currently everyone can read data and authenticated users can write.

  match /quizzes/{quizId} {
    allow read; 
    allow write: if request.auth != null;
  }

That works fine, but I also want unauthenticated users to write only to a specific key in a document.

Example content of a document:

{
  title: 'title',
  plays: 12,
  playedBy: [//Filled with user id's],
  ...
}

Is there any way that limits unauthenticated users to only have write access to the playedBy array and not the other keys of that document?

2

2 Answers

5
votes

Sure thing. But it may become a bit involved if you have a lot of fields.

Let's start with the simplest example. Something like this allows an unauthenticated user to write the playedBy as long as that is the only field in the document:

if request.auth != null || request.resource.data.keys().hasOnly(['playedBy'])

This works if the unauthenticated user is creating a new document, or updating an existing one. But it will stop as soon as the document contains more fields, since request.resource.data contains all fields the document will have after the write succeeds.

So the better alternative is to check that only the playedBy is modified, and that all other fields have the same value as before. The tricky bit there is handling the non-existence of fields, which I typically handle with a few helper functions:

function isUnmodified(key) {
  return request.resource.data[key] == resource.data[key]
}
function isNotExisting(key) {
  return !(key in request.resource.data) && (!exists(resource) || !(key in resource.data));
}

And then:

if request.auth != null &&
  request.resource.data.keys().hasOnly(['title', 'plays', 'playedBy']) &&
  isUnmodified('title') && 
  isUnmodified('plays')

The exact rule might be a bit off, but I hope this is enough to allow you to complete it yourself.

4
votes

After the earlier answer (late 2019, I believe), Firebase has brought in Map.diff.

Something like:

match /quizzes/{quizId} {
    allow read; 
    allow write: if request.auth != null ||
      request.resource.data.diff(resource.data).affectedKeys() == ["playedBy"].toSet()
  }

Tested code where I use it can be seen here.