Firestore rules - use exists with where - google-cloud-firestore

In my project I have firestore collection of users and classes. Each user can be part of one or more classes. Classes document has property members which is an array including all users uids in that class.
For instance:
users documents:
doc.id: USER1UID
{ name: 'user1', email: 'user1#user1.com', phone: '+123 456 789 001' }
doc.id: USER2UID
{ name: 'user2', email: 'user2#user2.com', phone: '+123 456 789 002' }
doc.id: USER3UID
{ name: 'user3', email: 'user3#user3.com', phone: '+123 654 789 003' }
classes documents:
doc.id: ABCDEF
{ name="class1", members: ['USER1UID', 'USER2UID'] }
doc.id: GHIJKL
{ name="class2", members: ['USER1UID', 'USER3UID'] }
doc.id: MNOPQR
{ name="class3", members: ['USER3UID'] }
I need to write a rule that will allow user to read details about another user ONLY if they are in the same class. Every user can also read own profile.
In this case user1 can read details of user2 and user3. (they are together in class1 and class2).
User2 can read details only of user1 (they are together in class1).
User3 can read details only of user1 (they are together in class2).
I need something like:
match /users/{userId} {
allow read:
//user is logged in
if request.auth != null
&& (
//user can read own profile
request.auth.id == $(userId)
//there is a class where are both (requesting and requested) users
|| exists( (/databases/$(database)/documents/classes/).where(request.auth.id in members).where($(userId) in members)
)
}

What you're trying to do is not possible with your database schema, because security rules don't allow you to perform queries. You may only request one document at a time using its known path, maximum of 10 documents per rule execution.
What you can do instead is manage each users document to contain a list of all other users who have a class in common with that user. But you will have to write some code to keep that up to date as the roster of the classes might change over time. This might be a good use for Cloud Functions.

Related

Complex firestore rules

I have a top level collection: "organizations", in that collections doc's there is an employees map like this:
employees: {
uid1: {
displayName: John Do
[...]
}
uid2 {
[...]
}
}
I have an other top collection: "customers" with an organization map like this:
organizations: {
organizationId1: some string,
organizationId2: some other string,
[...]
}
where:
uid is the user id from firebase auth
organizationId is the document id of an organization doc.
user can be in multiple organizations, and customers can be share between multiple organizations as well.
I want to restain acces to customer doc, at user who are employee of at least one organization listed in the customer doc.
Has there is no way to loop in firestore.rules
I think the answer may be mapDiff, and custom claims.
user custom claims:
organizations:[organizationId1, organizationId2, ...]
But i have some difficulty to understand the documentation:
https://firebase.google.com/docs/reference/rules/rules.MapDiff
Is there a way to achive that ?
Maybe I didn't understand it correctly, but you can try something like this:
allow read, write: if employeeOrganization in [organization1, organization2...]
https://firebase.google.com/docs/reference/rules/rules.List
I finaly find the ways to set rules for that:
match /customer/{cId} {
allow create: if request.auth != null;
allow read, update: if (request.auth.token.organisations.keys().hasAny(resource.data.organizationIds.keys()));
allow delete: if false;
}
My custom claims are like:
(there is only 1 to 5 organisations so it's not heavy to transmit)
organizations:{
organizationId1: "role",
organizationId2: "admin",
...
}
My file customer docs as a map like this:
organizationIds{
organization1Id: "some AES key used for security purpose",
organization358Id: "some AES key used for security purpose"
}
It work nice and using custom claims save countless read per day.

Firestore rules and data structure

I have a question regarding data structure and rules ... I have content on which users can vote. Something like this:
Firestore object:
{
name: "Cat",
description: "A cat named Cat",
votes: 56
}
Now ... I want authenticated users to be able to have update access to the votes, but not to any other values of the object and of course read rights since the content has to be displayed.
I did this because I wanted to avoid additional queries when displaying the content.
Should I create another collection "votes" maybe where the votes are kept and for each document make an additional request to get them?
In rules, you have access to the state of the data both before and after the writes - so you can test specific fields to be sure they have not changed:
function existing() {
return resource.data;
}
function resulting() {
return request.resource.data;
}
function matchField(fieldName) {
return existing()[fieldName] == resulting()[fieldName];
}
....
allow update: if matchField("name") && matchField("description")
....
The functions just make the rule easier to read.

MongoDB User notifications scheme suggestions

I was wondering what is the best scheme for user/notifications kind of scenario like the following:
1 | 01-04-2020 | X | John liked your post2
2 | 01-03-2020 | X | Mike and 9 other persons liked your post1
3 | 01-02-2020 | | Rose and 2 other persons liked your post1
4 | 01-01-2020 | | Bernard liked your post1
x = notification has not been read by the user yet
post1 = the same post in all the notifications
Let's say I have a Notification collection like:
_id: ObjectID
receiver: ObjectID (User)
sender : ObjectID (User)
type: String ("post", "comment", etc...)
typeID: ObjectID (Post, Comment, etc...)
message: String
isRead : Boolean
timestamp: Date
A User collection like :
_id: ObjectID
username: String
.
.
.
email: String
A Post Collection like :
_id: ObjectID
postedBy: ObjectID (User)
.
.
.
content: String
A Like collection like :
_id: ObjectID
type: String ("post", "comment", etc...)
typeID: ObjectID (Post, Comment, etc...)
likedBy: ObjectID (User)
On the 01-01-2020, the user opened his notifications panel. Between the last time he checked his notifications and this date, only 1 person liked his post1.
On the 01-02-2020, the user opened his notifications panel. Between the last time he checked his notifications (01-01-2020) and this date, 2 persons liked his post1.
On the 01-04-2020, the user opened his notifications panel. Between the last time he checked his notifications (01-02-2020) and this date, 9 persons liked his post1 and 1 person liked his post2.
I want the user to be able to see all his previous notifications as well as the notifications he hasn't read yet. If the user has several notifications for the same post (X people liked his post since the last time he checked his notifications), I want to group them as 1 notification (I will mark all of them as read once he read that one grouped notification).
How can I do that?
Please let me know if you need more information or if I am being unclear.
Thanks
Edit:
I'm having a hard trying to figure out how to aggregate those notifications. I think I need some kind of read date marker as well to group the notifications that were grouped and read at the same time, but maybe I need another collection to store the grouped notifications?
Notification.aggregate([
{
$group: {
_id: {
typeID : "$typeID",
receiver: "$receiver",
isRead: "$isRead"
// maybe something with a read date?
},
count: {$sum: 1}
}
}
])
I guess this article at Mongo University is a relevant answer to your question.
Use at least two collections: users and notifications also, you your users _id field isn't something like name and you'll allow them to be renamed, then it's perfect to have 3-rd collection likes, instead making likes as a embedded documents in array, like this:
User's schema:
_id: ObjectID,
likes: [{
_id: ObjectID //like_id,
other: "field"
}]
Notifications:
_id: ObjectID
receiver: ObjectID (User)
sender : ObjectID (User)
type: {
type: String,
enum: ["Post", "Comment"] /** Use enum for avaliable values */
},
typeID: {
type: ObjectID, /** It's better if every ID field have an index */
index: true,
unique: true
}
message: String
isRead : Boolean
timestamp: {
type: Date,
default: Date.now /** Don't forget about default values, but check my advice below about $timestamps */
}
Not sure that timestamp field is needed for you, as for me, it's
better to use {timestamps: true} option.
Also, every field with ObjectID should be indexed, it you needed
this fields for aggregation framework. It's a perfect performance
case for $lookup
I want the user to be able to see all his previous notifications as well as the notifications he hasn't read yet.
You needed a compound index for this, like {sender:1, createdAt: -1, isRead: 1}
I want to group them as 1 notification (I will mark all of them as read once he read that one grouped notification).
This is a job for aggregation framework, via:
{
$match: { query_criteria },
},
{
$group: { query_group_by $notification.typeID }
}
So your schema is fine, it's possible to do that. By the way, to test your own queries, you could use MongoPlayground, instead of production DB.
As for the likes schema, it's for you to decide, but maybe it's better to have them as an embedded (child) documents, like:
Post
_id: ObjectID
postedBy: ObjectID (User)
likes: [{
/** Likes Sub-schema */
}]
content: String
Take a look at sub-schema pattern in mongoose.
Hope it will helps you!

Should I have different collections for different users in mongodb?

Im new to databases and mongo. I'm creating a web app that has different types of users that have access to different routes via different UI's. eg: user, company, admin. My question is should I create a single collection that houses all users and simple add a "user-type" or "access-level" as a property on each User object? or should I have 3 different Collections, one for each type of user?
What is common practice for this type of thing?
What would be fields for each type of user? If they are same, use user type option. Same user can be in multiple roles tomorrow. Storing in same collection would be better.
If the fields to be stored are completely different, and there is not a chance that same user can be in 2 roles ever in your application, use 3 collections.
By your question your Schema can have Roles set as ENUM and value as user, company, admin. and role should be accordingly set while you save the data to db
var UserSchema = new Schema({
first_name: {
type: String,
required: true
},
last_name: {
type: String
}, Roles: {
type: String,
enum: ['USER', 'COMPANY', 'ADMIN'],
default: 'ACTIVE'
}
});
UserSchema.index({
username: 1,
role: 1
});

Rest API get resource id by field

What is a correct rest way of getting a resource ID by a field, for example a name. Take a look at the following operations:
GET /users/mike-thomas
GET /users/rick-astley
I don't want to use these operations at my API end, instead I want to write an API operation that will get me the ID when submitting a field (name in the case of users) for example:
GET /users/id-by-field
Submitted data:
{
"fullName": "Mike Thomas"
}
Return data:
{
"data": {
"id": "123456789012345678901234"
}
}
What you want is known as an algorithmic URL where the parameters for the algorithm are passed as URL parameters:
GET /users?name="Mike Thomas"
Advantages are that you are using the "root" resource (users) and the search parameters are easily extended without having to change anything in the routing. For example:
GET /users?text="Mike"&year=1962&gender=M
where text would be searched for in more than just the name.
The resultant data would be a list of users and could return more than the identification of those users. Unless fullName uniquely identifies users, that is what you need to allow for anyway. And of course the list could contain a single user if the parameters uniquely identified that user.
{
users: [
{
id: "123456789012345678901234",
fullName: "Mike Thomas",
dateJoined: 19620228
}
, {
id: "234567890123456789012345"
fullName: "Rick Astley",
dateJoined: 19620227
}
]
}