2
votes

I Have a Google Firestore realtime database, which contains a flag isPublic. If set to true, the child node details can be read by unauthenticated users. If set to false, the record can only be accessed by one (for now) authenticated & authorized user.

The authorized user is the only one with write permission.

Further, the child node private always only grants access to the one authorized user.

The solution I have looked into is based on query based rules https://firebase.google.com/docs/database/security/securing-data#query-based_rules Unfortunately, I am getting a permission_denied response, even as the authorized user.

Info aside: this is in a Vue JS project using the Firebase SDK.

Here is my database sample.

The first product should have read access to details for unauthenticated users, but not to private.

The second one should not be shown to unauthenticated users at all, by forcing them to run the query dbRef.orderByChild('isPublic').equalTo(true)

"products": {
  "-pushIdxyz1" : {
    "isPublic" : true,

    "private" : {
      "notes" : "lorem ipsum always private",
      "soldDate" : ""
    },

    "details" : {
      "imageURL" : "https://firebasestorage.imagexyz",
      "keywords" : "this, that",
      "title" : "Public product title",
      "workCategory" : "hardware",
    },
  },
  "-pushIdxyz2" : {
    "isPublic" : false,

    "private" : {
      "notes" : "lorem ipsum always private",
      "soldDate" : ""
    },

    "details" : {
      "imageURL" : "https://firebasestorage.imagexyz",
      "keywords" : "this, that",
      "title" : "Secret product title",
      "workCategory" : "hardware",
    },
  }
}

And my not-working rules

"rules": {
  "products": {
    "$product_id": {
      ".indexOn": "isPublic",
      ".read": "query.equalTo == true || auth.uid == '[admin user id]'"
      // alternative attempt
      // ".read": "(query.orderByChild == 'isPublic' && query.equalTo == true) || auth.uid == '[admin user id]'",
      ,
      ".write": "auth.uid == '[admin user id]'",

      "details": {
        ".read": true,
      },
      "private": {
        ".read": "auth.uid == '[admin user id]'",
      }
    }
  }
}

This query, for unauthorized users, should give read access to the details child node:

firebase.database().ref('products').orderByChild('isPublic').equalTo(true).once('value')
  .then(...)

This query, for the authorized user only, should give read access to all data

firebase.database().ref('products').once('value')
  .then(...)

With the current setting I am getting "permission denied" for either query, logged in as the authorized user or not.

2

2 Answers

1
votes

If you want a user to be able to read from the products node, you will need to have a .read rule on that node. And since you only want to allow the read if it's a query for public products, you should validate that specific query.

Something like this:

"rules": {
  "products": {
    ".indexOn": "isPublic",
    ".read": "(query.orderBy == 'isPublic' && query.equalTo == true)
              || auth.uid == '[admin user id]'"
  }
}

So:

  1. You defined the rule one level too low, which means all reads on /products were rejected.
  2. Your rule wasn't checking the orderBy field.

Note that the additional .read rule you've declared on private is meaningless here, since the same user already got read permission on the entire `products node anyway.

0
votes

Franks answer pointed me into the right direction (thank you!), but I decided to post a more complete answer myself.

Here's the data structure. Note that I changed 'isPublic' to 'isPublished'. 'products' now contains two child nodes: 'private' and 'public'. Both use the same key.

// Firebase Data
"products": {
  "private" : {
    "-pushId_1" : {
      "notes" : "Private notes product 1",
      "soldDate" : ""
    },
    "-pushId_2" : {
      "notes" : "Private notes product 2",
      "soldDate" : ""
    }

  },
  "public" : {
    "-pushId_1" : {
      "imageURL" : "https://firebasestorage.imagexyz",
      "isPublished" : true,
      "title" : "Published product title",
    },
    "-pushId_2" : {
      "imageURL" : "https://firebasestorage.imagexyz",
      "isPublished" : false,
      "title" : "Unpublished product title, only accessible if authorized",
    }
  }
}

The rules allow admin everything and allow unauthenticated access to the public child node if the query looks for published products

// Firebase Rules
{
  "rules": {
    "products" : {
      ".read": "auth.uid == '[admin user id]'",
      ".write": "auth.uid == '[admin user id]'",

      "public" : {
        ".indexOn": "isPublished",
        ".read": "(query.orderByChild == 'isPublished' && query.equalTo == true)"
      }
    }
  }
}

To create new products, I am first pushing to 'public', then use the returned key to create the private record

// Add new product
db.ref('products/public').push(publicData)
  .then((data) => {
    key = data.key
    return key
  })
  .then((key) => {
    db.ref('products/private').child(key).update(privateData)
    return key
  })
  .then( ... )

To get the products, public/unauthenticated users have to look for isPublished in public. The admin can loop through public and private child nodes

// Retrieve products - Public
db.ref('products/public').orderByChild('isPublished').equalTo(true).once('value')
  .then((data) => {
    let publicData = data.val()

    // Do something with the data
  })

// Retrieve products - Admin
db.ref('products').once('value')
  .then((data) => {
    let publicData = data.child('public').val()
    let privateData = data.child('private').val()

    // Do something with the data
  })