Accessing a deeper level document if email is found along the way in cloud firestore - firebase-security

I'm a teacher who has built a webapp for my school to store assessment data basically. The database has this structure:
matrix -> mStudents -> mStudents--active -> *STUDENT* -> courseList -> *COURSE* -> assessments -> *ASSESSMENT*
there are many STUDENT documents, and each have a few COURSE documents, and each have a few ASSESSMENT documents - this is where the grade data is. Initially, I allowed teachers to write freely, but I want to up the security level a bit and allow teachers to only write in COURSE documents (and deeper than that) that have their email listed in them. Here is what I've tried:
Attempt #1
match /matrix/mStudents/mStudents--active/{studentDoc=**} {
allow write: if request.auth.token.email == resource.data.teacher_email;
}
This would work(?) if all the ASSESSMENT documents also had teacher_email in them, but I would much rather prefer a cleaner solution.
Attempt #2
match /matrix/mStudents/mStudents--active/{student}/courseList/{course=**} {
allow write: if request.auth.token.email == get(/databases/$(database)/documents/matrix/mStudents/mStudents--active/$(course)).data.teacher_email;
}
I feel like this should work but alas no luck here. Maybe the recursive wildcard can't be used as a variable?
This is basically my question then: is it possible to access a deeper document by retrieving user credentials along the way, or should I just try to use attempt #1?

In your failed attempt you are missing the student collection within the get
Try this:
match /matrix/mStudents/mStudents--active/{student}/courseList/{course} {
allow write: if request.auth.token.email == resource.data.teacher_email;
}
match /matrix/mStudents/mStudents--active/{student}/courseList/{course}/assessments/{assessment} {
allow write: if request.auth.token.email == get(/databases/$(database)/documents/matrix/mStudents/mStudents--active/$(student)/courseList/$(course)).data.teacher_email;
}

Related

How to allow firebase user to only access documents that they created

This, to me, is the most basic authentication scheme for user-generated content, given a collection called "posts":
Allow any authenticated user to insert into "posts" collection
Allow the user who inserted the document into collection "posts", to read, update, and destroy the document, and deny all others
Allow the user to list all documents in collection "posts" if they are the one who created the documents originally
All examples I've found so far seem to rely on the document ID being the same as the user's id, which would only work for user's "profile" data (again, all the examples seem to be for this single limited scenario).
It doesn't seem that there is any sort of metadata for who the authenticated user was when a document was created, so it seems i must store the ID on the doc myself, but I haven't been able to get past this point and create a working example. Also, this opens up the opportunity for user's to create documents as other users, since the user ID is set by the client.
I feel like I am missing something fundamental here since this has to be the most basic scenario but have not yet found any concise examples for doing this.
This answer is from this github gist. Basically, in the document collection posts there is a field called uid and it checks if it matches the users uid.
// Checks auth uid equals database node uid
// In other words, the User can only access their own data
{
"rules": {
"posts": {
"$uid": {
".read": "$uid === auth.uid",
".write": "$uid === auth.uid"
}
}
}
}
-- Edit --
DSL rules
match /Posts/{document=**}{
allow read : if uid == request.auth.uid;
allow write: if uid == request.auth.uid;
}

Firestore Database Rules for User

I'm following a tutorial about firestore but I don't understand firestore rules very well. I'm trying to allow anyone to be able to create in the standard
users/uid/
path but only allow updates if the requester is trying to update
users/theirUserId/
I saw this in the documentation, but it didn't seem to work for me:
allow write: if request.auth.uid == resource.data.author_id;
Can anyone explain the functionality of the above line and/or offer suggestions as to how I can achieve this?
Additionally, is there any way to specify rules for a specific piece of data within a document?
It looks like that your document doesn't contain a author_id field.
The Firebase documentation Writing Conditions for Security Rules use this example:
service cloud.firestore {
match /databases/{database}/documents {
// Make sure the uid of the requesting user matches the 'author_id' field
// of the document
match /users/{user} {
allow read, write: if request.auth.uid == resource.data.author_id;
}
}
}
It means that a random user will be able to read and write in the users collections only if their authentication ID equals the author_id field of a specific document.
The resource variable refers to the requested document, and resource.data is a map of all of the fields and values stored in the document. For more information on the resource variable, see the reference documentation.
For your second question, I recommend you to have a look on the documentation about resource variable (link in the quote above). It is the same logic as your author_id question.
You can split allow write in to three create, update, delete for specific cases.
In your case
allow create: if request.auth.uid != null;
allow update: if request.auth.uid == resource.data.author_id;
which says any authenticated users can create and only update their on document. and created user must have a field author_id which is their user id.

How to filter fields in documents with security rules

I am experimenting with Cloud Firestore security rules. Is it possible to filter document fields?
For example if you have a document
{
name: "John Doe",
email: "doe#example.com"
}
then some users aren't allowed to get the document with the email address. Their application requests the document with
firebase.firestore.doc('users/doe-uid')
and gets this document
{
name: "John Doe",
}
If yes, how?
I think it should be possible because the Cloud Firestore Security Rules Reference says in the first sentence (emphasis is mine):
Cloud Firestore Security Rules are used to determine who has read and write access to collections and documents stored in Cloud Firestore, as well as how documents are structured and what fields and values they contain.
However I couldn't find anything in the reference telling me how to filter out fields.
Firestore rules are not filters, they're a server-side validation of document queries, meaning that you access (or not) the whole document, not particular fields.
The piece of documentation you mentionned means that you can do data validation on fields.
Here is a basic example of rules validating data on a write query (via request.resource.data) :
match /users/{userId} {
allow write: if request.resource.data.age is int;
}
Here is another basic example that uses an existing field to validate a read query (via resource.data) :
match /articles/{articleId} {
allow read: if resource.data.isPublished == true;
}
To filter out fields, you have do it client side, after the query.
Now If you want to secure access to certain fields, you have to create another collection (look into subcollections) with a different set of rules, and make another query that will match these rules.

Firebase simple blog (confused with security rules)

I'm trying to create a simple todo or blog system based on React + ReactFire.
And after a hour of reading firebase tutorial confused about configuring firebase security rules.
Code for saving element :
this.props.itemsStore.push({
text : this.state.text,
done : false,
user : this.props.user.uid
})
Everything ok, but how i can get all records what owns only but authorized user?
This rules doesn't works :
"rules": {
"items" : {
".write" : "auth !== null",
"$item" : {
".read": "data.child('user').val() == auth.uid"
}
}
}
Seems to there no way to get all records only for one user, with security rules, instead of this, i should use something like filter. But again, i don't know how to filter elements in ReactFire, and in manuals no information.
As example how does it work in Parse http://i.stack.imgur.com/l9iXM.png
The Firebase security model has two common pitfalls:
permissions cascade: once you've granted a read or write permission on a specific level, you cannot take this permission away at a lower level
rules are not filters: (this is essentially a consequence of the previous pitfall) you cannot use security rules to return a different subset of children for specific users. Either a user has access to a node, or they don't have access to it.
You seem to be falling for that second pitfall. While the user can access each specific message that they are the user for, they cannot query the higher-level items node since they don't have read access to it.
If you want to secure a list of messages/todos for a specific user, you will need to store that data for that specific user.
items_per_user
$uid
$itemid: true
This is quite common in NoSQL database and is often called denormalizing. See this article called "denormalization is normal" on the Firebase web site. It's a bit outdated as far as the Firebase API goes, but the architectural principles on denormalizing still apply.
To then show the items for a user, you'd do:
ref.child('items_per_user')
.child(ref.getAuth().uid)
.on('child_added', function(snapshot) {
ref.child('items')
.child(itemId.key())
.once('value', function(itemSnapshot) {
console.log(itemSnapshot.val());
});
})
Many developer new to Firebase think that the inner loop will be too slow to load their data. But Firebase is very efficient when it comes to handling multiple requests, since it only opens a connection once per client and pipelines all the requests in the inner loop.
Keep in mind, Rules are not filters. They allow access to nodes based on criteria.
Here's an example simple structure where users 0 and 1 have stored text data within their node.
Data Structure
ToDo
a_user_id_0
text: "some text"
done: yes
a_user_id_1
text: "another text"
done: no
Rules
In this example rule, users can only read/write from nodes that belong to them within the ToDo node, so the path $user_id would be equal to their auth.id. It assumes the users has authenticated as well.
"ToDo": {
"$user_id": {
".read": "auth != null && $user_id == auth.uid",
".write": "auth != null && $user_id == auth.uid"
}
}
If user_0 was auth'd and attempted to read/write data from a_user_id_1 node, it would fail.

yii rbac: check autorizations on groups instead of users

I have a question about the rbac system. I think I've pretty well understood it but I need more informations about a special case.
I would like to do the autorisations on groups instead of users. I mean for instance the group "HR" has permission to create a person. Then any person who join this group would have it as well.
Let me give you more informations.
A part of my database:
And this a part of what my group hierarchy could be:
So what I'm looking for, this would be a must, is a system where each group has some autorizations. People get the autorizations of their group and of their parents group (for instance people in "Forsys" has the autorizations of "Forsys", "R&D" and "Administration").
The solution I see at the moment is using bizrule. But I'm not sure write php code in database is a good idea and then if I update the group hierarchy (R&D inherits of RH instead of Administration) I would have to modify bizrule in database. I tried it and it works well but as you can see it require a lot of code.
$user = User::model()->with("people","people.groups")->findByPk(Yii::app()->user->id);
foreach($user->people[0]->groups as $group)
if($group->id == 2)
return true;
return false;
It's just for see if a user is in a group (without checking parent groups and hierarchy)
Another possibility could be create a new table "group_auth" where we would say for instance:
-Group_2 has role "managePerson"
-Group_3 has operation "deleteUser"
...
And then everytime a user is added in or removed of a group we would update his autorizations in the auth_assigment table.
I'd like to hear other opinions on this subject.
All comments will be appreciated :)
Thank you for reading and sorry for my English if you had difficulties to understand me.
Michaƫl S.
Do users ever get their own authorization items? If not, seems like you could in essence swap out the userid column in auth_assignment and name it / treat it as groupID instead. That way you wouldn't need to worry about keeping user auth assignments in sync with your group roles.
A couple of places you'd probably need to make some changes:
- by default CWebUser passes in the logged in userid for use in bizrules. Might be good to change that our with your own override that passes in groupId/groupIds instead.
- you'd need to override CDbAuthManager and rework some of how things work there
We've done something similar on a project I've worked on (we were handling multi-tenant RBAC custom permissions), which required custom CDbAuthManager overrides. It gets a bit tricky if you do it, but there is an awful lot of power available to you.
Edit:
Understood about your users sometimes needing to have additional authorizations. What if your group has a 'roles' field with different roles serialized in it (or some other method of having multiple roles stored for that group, could also be a relationship).
Then, on user login (for efficiency), you'd store those roles in session. Probably the easiest way to handle things would be to write a custom checkAccess for your WebUser override:
https://github.com/yiisoft/yii/blob/1.1.13/framework/web/auth/CWebUser.php#L801
as that will make things simpler to do your custom checking. Then I'd probably do something like:
if(Yii::app()->user->hasGroupAccess() || Yii::app()->user->checkAccess('operation/task/role')) {
....
}
In your WebUser hasGroupAccess method, you could loop over all group roles and send those to checkAccess as well.
Think that will work?
What I use to check access for groups when it's in another table, or somewhere else in the application I give the user the role per default. By using this:
return array(
'components'=>array(
'authManager'=>array(
'class'=>'CDbAuthManager',
'defaultRoles'=>array('authenticated', 'R&D', 'Administration'),
),
),
);
Under: Using Default Roles
By using this, every user gets these assignments. Now, I create a business rule to make sure that the checkAccess('group') will return the correct value.
For example in your case the business rule for R&D would be:
return (
count(
Person::model()->findByPk(Yii::app()->user->id)->groups(array('name'=>'R&D'))
) > 0
) ? true : false;
So what this does is:
find the logged-in person by primary key
look into groups (from the user) for the group with name R&D
if there is a group: return true (else return false)