Firestore Security Rules: How to get document and check if resource property is true - google-cloud-firestore

I need some help writing a Firestore security rule.
I have a subcollection called posts like the following example path:
/users/XrMD3azk4Jess5KNTSICv4RYEj02/posts/what-architecture-style-is-this_r3nosJHQIt
I am building a "mystery blog" app where users can submit mysteries (aka, "posts") and other users can provide answers (aka, "comments") to the mystery post. If the mystery post author feels that the provided comment solves the mystery, then the post author can go ahead and click a button labeled "Select as answer".
Example screenshot:
The answer is saved to a subcollection called answers in the user's record (the user that provided the answer, aka "doss"). This is an example path with screenshot:
This works fine.
However, I obviously only want the post author to be allowed to press the "select as answer" button...and I will write the front-end logic for that so that button only appears for the post owner. But, how can I correctly write the back-end firestore rule to prevent a non-post owner user from maliciously trying to edit/select an answer?
I assume that I need to check the post author's id (aka, uid) and see if it matches the signed in user's uid.
This is what I tried, but I get permission errors here:
match /users/{uid}/answers/{docId} {
allow read;
allow write: if request.auth != null; && get(/databases/$(database)/documents/users/$(request.auth.uid)/posts/$(id)).data.uid == uid
}
Here's an example of the post document that contains the author's uid and the post id:
Thanks for any help or pointers!

So I went a different path just to make this a bit easier on the security rule logic and maintenance.
I simply added a postUid property when creating the answer doc:
async selectAsAnswer(comment) {
const answerDoc = this.$fire.firestore
.collection(`users/${this.comment.uid}/answers`)
.doc()
await answerDoc.set({
id: answerDoc.id,
commentId: comment.id,
postId: comment.postId,
postUid: this.post.uid,
createdAt: this.$fireModule.firestore.FieldValue.serverTimestamp(),
commentUid: comment.uid
})
}
Then, in the security rule for this answer document I checked for the postUid property match:
match /users/{uid}/answers/{docId} {
allow read;
allow create, update: if signedInAndVerified() && request.resource.data.postUid == request.auth.uid;
}
This seems to work fine and it just makes the overall DX a bit easier.

Related

Flutter Firestore Security Rules

I've been trying to get the Firestore rules to play nice for a while now and every time I think I get them right, another portion stops working for some reason.
I'm trying to have some simple rules, if you made the document that document and any child documents or collections, you can create, edit and delete them. I thought this was pretty simple but alas I keep getting permission denied errors.
Rules:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, update, delete: if request.auth != null && request.auth.uid == userId;
allow create: if request.auth != null;
}
match /users/{userId}/{document=**} {
allow read, update, delete: if request.auth != null && request.auth.uid == userId;
allow create: if request.auth != null;
}
}
}
When doing just the match /users/{userId} I was able at one time able to create user documents but I couldn't create child documents or colletions.
When doing just the match /users/{userId}/{document=**} I could no longer create users but any existing users I could add child documents and collections and do everything expected.
This combination of both rules doesn't seem to work either.
I keep getting [cloud_firestore/permission-denied] The caller does not have permission to execute the specified operation. when I try to create a user with this statement:
await FirebaseFirestore.instance.collection('users').doc(googleUser.uid).set(
{
'created': now,
'lastLogin': now,
'name': name,
'email': email,
},
);
Now nothing works. I deleted all my authentication accounts and my Firestore data and wanted to start over but it simply will not create the data in Firestore.
Any suggestions would be greatly appreciated as I'm going in circles and nothing is working anymore which is extremely frustrating as it did at one point but no longer does.
edit All of my testing is being done on a real Android phone.
After walking away from my computer and thinking more, I figured out what it was. My App Check debug token changed somehow.
Once I added the new value from the debug console everything started working again.
I'll leave this answer here in case this saves anyone else some headaches in the future!
Edit: Additionally, ones App Check debug token will change anytime you clear storage on your app on the device. Which is why mine was changing.

Building user database model in Firebase

so I already finished all of the actual app for this. I just need to setup the backend. I figured Firebase was the best solution since Parse is no longer a thing. What I wanted was:
Users with profiles - These profiles can be viewed by added friends but only edited (written) to by the actual profile owner.
So I read through the Firebase Docs and still cannot really figure out how to do this. They only have 1 Swift application example that does not do anything similar and the one Obj C twitter one, will not even build. All of their docs still have println for Swift which just makes me think it is not updated frequently.
Does anyone have any good examples / tutorials of this? I keep trying to search for things but nothing is as similar enough to what I want. I am more looking on how to setup the db for each user and access it rather actually using Firebase in Swift.
As I wrote in my comment to your question, this answer is based on what we do in a real social app Impether using Swift + Firebase.
Data structure
Let's assume that you want to store the following information for a single user:
email
username
name
followers - number of people who follow a particular user
following - number of people who a particular user follows
avatar_url - url of their avatar
bio - some additional text
Since in Firebase everything is stored a JSON objects, you can store the above structure under node with path like users/$userId, where $userId is Firebase User UID which is created for each registered user if you use simple email/password Firebase authorization.
Firebase email/password authorization is described in their docs:
https://www.firebase.com/docs/ios/guide/user-auth.html
https://www.firebase.com/docs/ios/guide/login/password.html
Notice that there are both Obj-C and Swift snippets. I find Firebase documentation really great as it helped me a lot when I was building our app.
For the purpose of this answer let's assume that we have user with username jack and Firebase User UID equal to jack_uid (in reality this will be a string generated by Firebase).
Then an example data for this user will be store under a path users/jack_uid and can look like this:
{
"email" : "jack#example.com",
"username" : "jack",
"name" : "Jack",
"followers" : 8,
"following" : 11,
"avatar_url" : "http://yourstoragesystem.com/avatars/jack.jpg",
"bio" : "Blogger, YouTuber",
}
Firebase email/password authorization works really well, but let's be honest, if user wants to sign in into the app, it's a lot better for him to use his username than his email he gave while he registering his account.
In order to do that, we decided to store a mapping from usernames to user ids. The idea is that if user inputs his username and password in a login form, we use that mapping to retrieve his user id and then we try to sign him in using his user id and provided password.
The mapping can be stored for example under a path username_to_uid and looks like this:
{
"sample_username_1": "firebase_generated_userid_1",
"sample_username_2": "firebase_generated_userid_2",
...
"jack": "jack_uid",
"sample_username_123": "firebase_generated_userid_123"
}
Then creating a profile may looks like this and it's done as soon as registration of a new account was successful (this snippet is very close to the exact code we use in the production):
func createProfile(uid: String, email: String,
username: String, avatarUrl: String,
successBlock: () -> Void, errorBlock: () -> Void) {
//path to user data node
let userDataPath = "/users/\(uid)"
//path to user's username to uid mapping
let usernameToUidDataPath = "/username_to_uid/\(username)"
//you want to have JSON object representing user data
//and we do use our User Swift structures to do that
//but you can just create a raw JSON object here.
//name, avatarUrl, bio, followers and following are
//initialized with default values
let user = User(uid: uid, username: username, name: "",
avatarUrl: avatarUrl, bio: "",
followers: 0, following: 0)
//this produces a JSON object from User instance
var userData = user.serialize()
//we add email to JSON data, because we don't store
//it directly in our objects
userData["email"] = email
//we use fanoutObject to update both user data
//and username to uid mapping at the same time
//this is very convinient, because either both
//write are successful or in case of any error,
//nothing is written, so you avoid inconsistencies
//in you database. You can read more about that technique
//here: https://www.firebase.com/blog/2015-10-07-how-to-keep-your-data-consistent.html
var fanoutObject = [String:AnyObject]()
fanoutObject[userDataPath] = userData
fanoutObject[usernameToUidDataPath] = uid
let ref = Firebase(url: "https://YOUR-FIREBASE-URL.firebaseio.com/images")
ref.updateChildValues(fanoutObject, withCompletionBlock: {
err, snap in
if err == nil {
//call success call back if there were no errors
successBlock()
} else {
//handle error here
errorBlock()
}
})
}
In addition to this you possibly want to store for each user a list of his followers and a separate list of users he follows. This can be done just by storing user ids at a path like followers/jack_uid, for example it can look like this:
{
"firebase_generated_userid_4": true,
"firebase_generated_userid_14": true
}
This is the way we store sets of values in our app. It very convenient, because it is really user to update it and check if some value is there.
In order to count the number of followers, we put this counter into user's data directly. This makes reading the counter very efficient. However, updating this counter requires using transactional writes and the idea is almost exactly the same as in my answer here: Upvote/Downvote system within Swift via Firebase
Read/write permissions
A part of your question is how to handle permissions to data you store. The good news is that Firebase is exceptionally good here. If you go to your Firebase dashboard there is a tab named Security&Rules and this is the place where you control permissions to your data.
What's great about Firebase rules is that they are declarative, which makes them very easy to use and maintain. However, writing rules in pure JSON is not the best idea since it's quite hard to control them when you want to combine some atomic rules into a bigger rule or your app simple grows and there are more and more different data you store in your Firebase database. Fortunately, Firebase team wrote Bolt, which is a language in which you can write all rules you need very easily.
First of all I recommend to read Firebase docs about Security, especially how does permission to a node influences permission for its children. Then, you can take a look at Bolt here:
https://www.firebase.com/docs/security/bolt/guide.html
https://www.firebase.com/blog/2015-11-09-introducing-the-bolt-compiler.html
https://github.com/firebase/bolt/blob/master/docs/guide.md
For example, we use rules for managing users data similar to this:
//global helpers
isCurrentUser(userId) {
auth != null && auth.uid == userId;
}
isLogged() {
auth != null;
}
//custom types, you can extend them
//if you want to
type UserId extends String;
type Username extends String;
type AvatarUrl extends String;
type Email extends String;
type User {
avatar_url: AvatarUrl,
bio: String,
email: Email,
followers: Number,
following: Number,
name: String,
username: Username,
}
//user data rules
path /users/{$userId} is User {
write() { isCurrentUser($userId) }
read() { isLogged() }
}
//user's followers rules
//rules for users a particular
//user follows are similar
path /followers/{$userId} {
read() { isLogged() }
}
path /followers/{$userId}/{$followerId} is Boolean {
create() { isCurrentUser($followerId) && this == true }
delete() { isCurrentUser($followerId) }
}
//username to uid rules
path /username_to_uid {
read() { true }
}
path /username_to_uid/{$username} is UserId {
create() { isCurrentUser(this) }
}
The bottom line is that you write rules you want using Bolt, then you compile them into JSON using Bolt compiler and then you deploy them into your Firebase, using command line tools or by pasting them into dashboard, but command line is way more efficient. A nice additional feature is that you can test your rules by using tools in Simulator tab in your dashboard.
Summary
For me Firebase is a great tool for implementing a system you want. However, I recommend to start with simple features and learn how to use Firebase in the first place. Implementing social app with functionality like for example Instagram is quite a big challenge, especially if you want to do it right :) It's very tempting to put all functionality there very quickly and Firebase makes it relatively easy to do, but I recommend to be patient here.
In addition, take your time and invest in writing tools. For example, we have two separated Firebase databases, one for production and second for testing, which is really important if you want to write unit and UI tests efficiently.
Also, I recommend building permission rules from the beginning. Adding them later may be tempting, but also quite overwhelming.
Last but not least, follow Firebase blog. They post regularly and you can be up to date with their latest features and updates - this is how I learnt how to use concurrent writes using fanout technique.

Meteor publish not working for private service.facebook.email publication

This is a Meteor/MongoDb question. In the code below, I am attempting to give all clients access to all users' Facebook names and Facebook profile Ids.
At the same time, I am attempting to give clients access to their own email as defined on their Facebook profiles.
Meteor.publish("users", function(){
return Meteor.users.find({}, {fields:
{'services.facebook.name': true,
'services.facebook.id': true}
})
});
Meteor.publish("mail", function () {
return Meteor.users.find({_id: this.userId},
{fields: {'services.facebook.email': 1}});
});
In the code below is how I have subscribed to the above publications.
Meteor.subscribe("users");
Meteor.subscribe("mail");
This should work as far as I am aware, yet when I attempt to make use of the user object, such as in the code below...
var user = Meteor.users.findOne({_id: Meteor.userId()});
console.log(user);
... I receive only the parts of the user object published as standard (profile.name & _id) and the parts of the user object published by the first meteor.publish function in the code above ("users"). The user's Facebook email is nowhere to be found. The code below demonstrates what I consistently keep receiving when I run the above findOne code.
{
_id: "3aNP7euRFGWhGEgq2",
profile: {name: "David Daish"},
services: {facebook: {name: "David Daish", ID: "1987779184781528"}}
}
As you can see, the Facebook email property is nowhere to be found. It's zilch, gone, not there. So there is my question:
How can I get services.facebook.email's value, and more importantly, how can I do it in a way that allows clients access to only their own services.facebook.email value?
Thank you very much in advance for your assistance. This has been confounding me for a couple days.

Firebase security rules for .indexOn : unique uid in iOS/Swift

I've seen several questions about placing specific security rules in Firebase. I've figured out how to do this in those cases. Now I'm saving information by authData.uid and getting this info....
[Firebase] Using an unspecified index. Consider adding ".indexOn": "4eb8920a-e407-4488-bce4-c6f64f7b0891" at /UserVideo to your security rules for better performance
How can I add such security rules for unique keys and how?? Here is my query...
let videoRef = self.ref.childByAppendingPath("UserVideo")
videoRef.queryOrderedByChild("\(currentUser)").queryLimitedToFirst(1)
.observeEventType(.ChildAdded, withBlock: { snapshot in
if snapshot != nil
Any insight is welcome!!
* UPDATE *
Yeah. I forgot to add that currentUser is...
let currentUser = ref.authData.uid
And the query actually gives back relevant information. It just also includes the specified warning. Will try the second answer a few times and update this post if it works. If anyone else has any ideas please post.
See this: Security & Rules Quickstart
Security and Firebase Rules are used to determine who has read and write access to your database as well to ensure the structure of that data. They are found in the Security tab of your App Dashboard. They come in three flavors: .write, .read, and .validate.
IN answer to your question, add security rules for unique keys by using the validate and write together.
An example would be:
{
"rules": {
"foo": {
// /foo is readable by the world
".read": true,
// /foo is writable by the world
".write": true,
// data written to /foo must be a string less than 100 characters
".validate": "newData.isString() && newData.val().length < 100"
}
}
}
Does this help?

How do I use a CouchDB username in a URL rewrite?

I have a list function that can be accessed like this:
_list/characters/characters_by_user?startkey=["org.couchdb.user:rodriguez"]&endkey=["org.couchdb.user:rodriguez", {}]
I'm attempting to rewrite the URL so you can access it in a friendlier way...
/rodriguez/characters
...by writing this rule:
{from: '/:user/characters', to: '_list/characters/characters_by_user',
query: {
startkey: "[%22org.couchdb.user%3A:user%22]",
endkey: "[%22org.couchdb.user%3A:user%22,%20{}]"
}
}
However, I get this error:
error: "query_parse_error",
reason: "No rows can match your key range, reverse your start_key and end_key or set descending=true"
Why would the query work correctly in the full URL, but not using the rewrite?
Update
I believe this may be a bug with the way CouchDB handles the encoded colon (%3A). I'm awaiting a response from the mailing list and will update this question accordingly.
I found that checking CouchDB's logs proved to be the best way to troubleshoot how URLs were being rewritten. Unfortunately, the issue I submitted to the CouchDB mailing list has yet to be replied to.
As a workaround, I've emitted the user's name without fully-qualifying it, which suits my purpose:
var user = doc.createdBy.split(":")[1];
emit(user, doc);