4

I'm using Firebase in my iOS app. I'd like each of my objects to have a creatorId property whose value is the authenticated user ID (authData.uid with a Firebase authentication object). I'm using a custom token generator for authentication, but the problem can be reproduced with anonymous log in too.

I'd like a user to only be able to read (and write, but let's focus on reading right now, as that's where I'm having my issues) objects that they created. In other words, the querying user's authenticated user ID will match the creatorId of the objects they are fetching.

I'm having trouble with permissions when I craft queries and rules to make this happen.

Here is the Firebase documentation for Rules and Security.

Here is what my Firebase dashboard looks like for a Task object:

+ firebase-base
    + tasks
        + {task_id}
             + creatorId: 
             + title: 

where task_id is a unique identifier generated by Firebase upon insertion.

My rules look like this (again, let's ignore writing rules for now):

{
   "rules": {
       "tasks": {
         "$task_id": {
           ".read": "auth.uid === data.child('creatorId').val()"          
         }
       }
   }
}

Reading a specific task works fine, but I'd expect to be able to make a query that says, "fetch all the tasks that I created" using observeEventType and related functions. This doesn't work for me. I get "Permission Denied" errors.

Here is how I'm observing, in Swift:

let reference = Firebase(url: "https://{My-Firebase-Base-Reference}/tasks")
reference.observeEventType(.ChildChanged, 
     withBlock: { (snapshot: FDataSnapshot!) -> Void in

                       // Success

             }) { (error: NSError!) in

                       // Error: I get Permissions Denied here.

              }

Per @Ymmanuel's suggestions, I also tried being more specific in my query, like so:

let reference = Firebase(url: "https://{My-Firebase-Base-Reference}/tasks")
reference.queryOrderedByChild("creatorId").queryEqualTo({USER_UID}).observeEventType(.ChildChanged, 
     withBlock: { (snapshot: FDataSnapshot!) -> Void in

                       // Success

             }) { (error: NSError!) in

                       // Error: I get Permissions Denied here.

              }

Neither of these blocks work, I always get "Permission Denied" errors. What am I doing wrong?

Tim Arnold
  • 8,359
  • 8
  • 44
  • 67
  • I have a question? with your rules you as "user A" you can create a task for "user B" as long as you set the "user B" uid in the creatorId right? – Ymmanuel Jun 04 '16 at 06:10
  • Also what is your output? your child added event gets fired at all? or not? sometimes is a success sometimes you get permission denied?? – Ymmanuel Jun 04 '16 at 06:12
  • I posted a couple of partial thoughts but the question is really unclear. What are you actually trying to do? If an existing node is observed via .ChildChanged, then if that node has changes, an event will be sent to your app. If a user should only be notified of changes in certain nodes, you would only attach observers to those nodes. There would be no rules necessary for that capability. If you want to disallow reading of a node that would be done with a rule. Can you clarify what your after? Perhaps a use case? – Jay Jun 07 '16 at 15:24
  • @Jay sorry for the confusion. I edited the question, hopefully improving clarity. Let me know if this helps. – Tim Arnold Jun 07 '16 at 17:18
  • Awesome update to you question. I updated my answer based on the fresh information. – Jay Jun 07 '16 at 18:17
  • 1
    Rules cannot be used to filter data. See http://stackoverflow.com/a/14298525/209103 for the best explanation of that. Also as Jay said: `ChildChanged` will only fire when an existing child gets changed, more likely you're looking for `ChildAdded`. But unless you address the "rules are not filters" requirement, nothing will work. – Frank van Puffelen Jun 09 '16 at 04:07
  • @FrankvanPuffelen thanks, that link was an excellent description of "Rules Are Not Filters". I've tried to improve my question so it better gets at my concerns. I would love any thoughts you have in response: http://stackoverflow.com/q/37727782/1148702 – Tim Arnold Jun 09 '16 at 13:46
  • I think perhaps I missed "Using Indices to Define Complex Relationships" in [Structuring Data](https://www.firebase.com/docs/web/guide/structuring-data.html), which may help quite a bit. – Tim Arnold Jun 09 '16 at 13:59
  • I have a followup question: You said *I'd like a user to only be able to read (and write, but let's focus on reading right now, as that's where I'm having my issues) objects that they created.* If that's what you are after, then why don't you just store said objects within that users node? Suppose it's a series of grocery items. /users/uid_0/grocery_items/item_0, item_1 etc.. Then create a rule that a user can only access (read/write) their own grocery items? That's a simple solution that seems to satisfy the parameters. – Jay Jun 09 '16 at 17:49
  • Because Firebase strongly suggests [flattening](https://www.firebase.com/docs/web/guide/structuring-data.html) your data structures. See my [other question](http://stackoverflow.com/q/37727782/1148702), I think it explains the answer to this problem well. – Tim Arnold Jun 09 '16 at 18:00

2 Answers2

3

What you are missing is that you are assuming that security rules are queries and that is not true.

Check the Rules are Not Filters section in the link.

Security rules only validate if you can read or write a specific path of your firebase database.

If you want to only receive changes of a specific user you should use firebase queries.

For example if you want to get all the tasks of a specific user, you should do:

let reference = Firebase(url: "https://{My-Firebase-Base-Reference}/tasks")
reference.queryOrderedByChild("creatorId").queryEqualTo(YOUR_CURRENT_USER_UID).observeEventType(.ChildChanged, 
     withBlock: { (snapshot: FDataSnapshot!) -> Void in
          // Success
     }) { (error: NSError!) in
          // Error: Get Permissions Denied Here.
}

This way you could get all the events only related to your user, and protect the information by applying the security rules.

also if you want to allow only the creator to write their own tasks you should also consider the case where you create the task and write something like this:

  "tasks": {
     //You need to include the $task_id otherwise the rule will seek the creatorId child inside task and not inside your auto-generated task
     "$task_id": {
       ".read": "auth.uid === data.child('creatorId').val()",
       //this is to validate that if you are creating a task it must have your own uid in the creatorId field and that you can only edit tasks that are yours...
       ".write":"(newData.child('creatorId').val() === auth.uid && !data.exists()) || (data.child('creatorId').val() === auth.uid && data.exists())",
       //you should add the index on to tell firebase this is  a query index so your queries can be efficient even with larger amounts of children
       ".indexOn":"creatorId",        
     }
   }

(check the syntax but that's the general idea)

LordParsley
  • 3,808
  • 2
  • 30
  • 41
Ymmanuel
  • 2,523
  • 13
  • 12
  • Thanks, your comments do inform some of what I was doing wrong. I'm still having a bit of trouble getting queries to work. Since my reference is at `tasks` in your example, and `creatorId` is actually a child of the individual task (under `taskId`), do I want to actually construct that query differently? It seems to me `creatorId` is the child of the child of my base reference in your example. – Tim Arnold Jun 06 '16 at 15:54
  • queryOrderedByChild('creatorId') combined with queryEqualTo(current-user-uid) is equal to say search in all the tasks which i don't know the $task_id, and bring me all the tasks whose creatorId is equal to current-user-uid – Ymmanuel Jun 06 '16 at 16:02
  • All of what you wrote makes sense, but I'm running into the same issue, where my `observeEventType` blocks are never called. – Tim Arnold Jun 06 '16 at 16:07
  • I'm not sure `queryEqualToValue` does what you think it does. Wouldn't this search the `tasks` endpoint for any objects with the value (`taskId` in this case) of `YOUR_CURRENT_USER_UID`? – Tim Arnold Jun 06 '16 at 16:10
  • i would recommend you doing this in two steps, first remove all the rules set the read and write to true, test the query is working and then add the rules, so you can identify if the error is in the rules or the query itself... once you verified we can work a solution – Ymmanuel Jun 06 '16 at 16:11
  • is not what i think it does...is what the documentation says it does.... https://www.firebase.com/docs/ios/guide/retrieving-data.html#section-queries – Ymmanuel Jun 06 '16 at 16:15
  • The queryEqualToValue: method allows us to filter based on exact matches. As is the case with the other range queries, it will fire for each matching child node. For example, we can use the following query to find all dinosaurs which are 25 meters tall: – Ymmanuel Jun 06 '16 at 16:15
  • by the way... i think your mistake is using only Child Changed( only will trigger if a node already exist and gets updated), maybe the event type you need for your functionality is Child Added(will receive all the children once you launch it) or a mix of childAdded and childChanged, try changing to ChildAdded – Ymmanuel Jun 06 '16 at 16:18
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/113949/discussion-between-tim-camber-and-ymmanuel). – Tim Arnold Jun 06 '16 at 16:40
1

A couple of comments:

If local persistence is on, and you have no internet connection and there is no local value, neither of the blocks will be called.

The observe code in the question is bugging me, it's not wrong but may be a bit clearer if it was like this:

    reference.observeEventType(.ChildChanged, withBlock: { snapshot in

            print(snapshot.value)

        }, withCancelBlock: { error in

            print(error.description)

    })

Edit:

The question is clearer now

I'd like a user to only be able to read objects that they created.

I'll demonstrate via the following:

A Firebase structure:

posts
  post_0
    created_by: uid_0
    msg: some message
  post_1
    created_by: uid_1
    msg: another message
  post_2
    created_by: uid_2
    msg: yippee

and the rules that will allow a user to only read from the post node they created

"rules": {
    ".read": false,
    ".write": false,
    "posts": {
      ".read": false,
      "$post_id": {
        ".read": "root.child('posts').child($post_id).child('created_by').val() == auth.uid",
        ".write": "auth != null"
      }
    }
  }

then the code to test. We assume the current users uid is uid_0:

let reference = self.myRootRef.childByAppendingPath("posts/post_0")

reference.observeEventType(.ChildAdded, withBlock: { snapshot in

        print(snapshot.value)

    }, withCancelBlock: { error in

        print(error.description)    
})

The above code will allow a read from node post_0 which is the post uid_0 created as indicated by the created_by child.

If the path is changed to posts/posts_1 (or anything else) for example, the read is denied.

This answer my not directly answer the question as if you are expecting the rules to 'block' or 'filter' the resultant data, that isn't what rules are for (per the other answer).

So you may want to go down a different path by constructing a query to pull out just the posts you want to work with based on the created_by = auth.uid like this

let reference = self.myRootRef.childByAppendingPath("posts")

reference.queryOrderedByChild("created_by").queryEqualToValue("uid_1")
    .observeEventType(.Value, withBlock: { snapshot in

            print(snapshot.value)

        }, withCancelBlock: { error in

            print(error.description)    
    })
Jay
  • 34,438
  • 18
  • 52
  • 81
  • This question has nothing to do with local persistence. I agree, that code is cleaner, I wanted to be explicit about the types in my examples. – Tim Arnold Jun 07 '16 at 17:19
  • @TimCamber The initial question said that neither of the blocks was called so I wanted to provide a possible reason that would happen. – Jay Jun 07 '16 at 17:26
  • This is helpful, but I'm not sure it addresses my issue. I'd like to observe `posts`, but only see events where `created_by` matches the querying client's `uid`. In this case, we can read the node for a specific object the user has already created, but what if a new `post` object is created on a different client device (same `created_by`)? How do we observe that change? Is this just impossible? – Tim Arnold Jun 08 '16 at 13:31
  • I understand "rules are not filters". I'd like rules and queries to work together, though. I'd like to fetch data based on a query ("fetch only objects that match my `created_by`"), but I'd also like to enforce similar access rules on Firebase. – Tim Arnold Jun 08 '16 at 13:33
  • I did exactly what you suggested at the end of your answer. However, whenever any `created_by` rules at in place, that `observeEventType` invocation always fails with "Permission Denied", as described in my question. – Tim Arnold Jun 08 '16 at 13:35
  • The last snippet of code does exactly that. If a node is added, edited or removed from /posts/ where the created_by = uid_1, the app will receive an event. I would suggest using .ChildAdded, .ChildChanged and .ChildRemoved instead of .Value as .Value returns everything that matches the query every time. The other's just return the node that was added, changed or removed. – Jay Jun 08 '16 at 13:35
  • And... for the last item to work, your rules would need to be update to read and write = true. Remember that you cannot filter your data with rules - they are meant to limit access to specific nodes and/or verify the format of the data being written. – Jay Jun 08 '16 at 13:36
  • Ok --- to be clear, there is no way for Firebase to have rules enforcing restricted reading of a subset of nodes (e.g. `posts`) while also allowing for general `observeEventType` queries on the parent node (e.g. `posts`)? – Tim Arnold Jun 08 '16 at 13:38
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/114136/discussion-between-tim-camber-and-jay). – Tim Arnold Jun 08 '16 at 13:39
  • @TimCamber I understand your comment but the statement isn't really how it's done. Firebase is about is how you store your data, not what data is stored. So, for example based on your comment. You actually have two sets of data - data that can be generally queried by anyone and another set of data that can only be queried by certain users. These could easily be stored within two different parent nodes. The key here is structuring data to work with how the user (and the developer) is expecting it; So yes, you can do what you are asking but not by leveraging Rules, as they are not filters. – Jay Jun 09 '16 at 17:34
  • See my refined [question and answer](http://stackoverflow.com/questions/37727782/filtering-and-permissions-in-firebase). Thanks for your help! – Tim Arnold Jun 09 '16 at 17:45