How to bind multiple query results in angularfire2 with firestore? - firebase

I would like to use a collection to maintain relation between some documents as proposed here (but slightly modified approach). I have chosen collection approach because it seems more scalable that document properties over time.
My actual data scructure
users
user1
user2
companies
co1
co2
user_co
user1
companies (collection)
co1
co2
user2
companies (collection)
co1
But with that approach, I need to do multiple queries in order to get all available companies for a specific user as we cannot perform "IN" clause.
So I need retrieve data in 2 steps:
retrieve list of company user have access to in user_co/{user}/companies
retrieve actual companies from /companies/{id}
Why 2 steps? Because I don't want to give read access to all companies to all users and querying /companies would then trigger access errorĀ­.
So I got struggle with how to retrieve a single bindable list of documents retrieved from multiple calls?
I got 2 items displayed in my component but field values does not get displayed. I certainly do something wrong in the way I retrieve /company documents.
Any help would be greatly appreciated.
MyService.ts
interface Company {
Name: string;
Owner: any;
id?: any;
time?: any;
creationTime?: any;
}
interface MyCompany {
id?: any;
Name: string;
}
#Injectable()
export class CompanyService {
companiesCollection: AngularFirestoreCollection<Company>;
myCompaniesCollection: AngularFirestoreCollection<MyCompany>;
myCompanies;
constructor(private afs: AngularFirestore, private ats: AuthService) {
this.myCompaniesCollection = this.afs.collection('user_co').doc(this.ats.currentUserId).collection('companies');
this.myCompanies = this.myCompaniesCollection.snapshotChanges().map(actions => {
return actions.map(a => {
// What is the good way to retrieve /companies data from here?
return this.afs.firestore.collection("companies").doc(a.payload.doc.id).get().then(doc => {
return { id: doc.id, ...doc.data() }
}).catch(error => {
console.log("Error reading company document:", error);
});
// Original example that return data from /user_co
//return { id: a.payload.doc.id, ...a.payload.doc.data() }
})
});
}
getData() {
return this.myCompanies;
}
}
using angularfire2 5.0.0-rc.3 with firebase 4.5.2

I've finally changed the way I store data into Firestore.
As mentionned here and in many docs related to NoSQL, denormalization is the way to go to avoid "join like" and multiple queries.
Using denormalization one can group all data that is needed to process
a query in one place. This often means that for different query flows
the same data will be accessed in different combinations. Hence we
need to duplicate data, which increases total data volume.
That way, I can simply retrieve /users/{user}/companies and get all relevant info about companies user belongs to. And there is no need to be able to access all company info (settings, users, etc) for all users anyway.
New data structure
/users/{user}
user_name
/companies/{company}
company_name
/companies/{company}
name
/admins/{user}
/users/{user}
user_name
Security rules allowing admins to invite/add users to company
match /users/{usr}/companies/{co} {
// Only visible to the actual user
allow read: if request.auth.uid == usr;
// Current user can opt-out of company
allow delete: if request.auth.uid == usr ||
// Company admin can add or drop-out a user
exists(/databases/$(db)/documents/companies/$(co)/admins/$(request.auth.uid));
// Company admin can add or drop-out a user
allow create, update: if exists(/databases/$(db)/documents/companies/$(co)/admins/$(request.auth.uid));
}
match /companies/{co} {
// Company accessible for members and admins only
allow read: if
exists(/databases/$(db)/documents/companies/$(co)/members/$(request.auth.uid)) ||
exists(/databases/$(db)/documents/companies/$(co)/admins/$(request.auth.uid));
match /admins/{usr} {
// allow company creation if it does not exists
allow create: if exists(/databases/$(db)/documents/companies/$(co)) == false
// updates allowed for admins
allow update, delete: if exists(/databases/$(db)/documents/companies/$(co)/admins/$(request.auth.uid));
}
match /users/{usr} {
// writes allowed for admins
allow write: if exists(/databases/$(db)/documents/companies/$(co)/admins/$(request.auth.uid));
}
}
Counterpart
When updating /companies/{company}/[name], I also need to retrieve all users that belongs to that company through /companies/{company}/users and then update all docs in /users/{user}/companies/{company}. This can be made within a single transaction.

Related

Firebase Firestore security rules: Using queries in security rules?

I am developing a web app on Firebase and Firestore, and here is what I am trying to do:
Users can upload their own posts, and set its visibility to public or private.
Any signed in user can see public posts, but only the users who are subscribing to the writer can view private posts. I am trying to write security rules for this.
Here is the database structure:
db.collection('posts').doc({postid})
//When a user writes a post, a new document is created. Includes the boolean 'public' field and the 'uid' field, which stores the writer's uid.
db.collection('subscription').doc({viewer_user_id})
//Once the logged-in user subscribes to another user, a document is created under the user's UID. The doc includes array of the UID of the users the viewer is subscribing.
Here are the descriptions of the relevant fields under each of the docs in 'posts' and 'subscription' collections:
posts/{postid}: {
'uid': Writer's uid (String)
'public': Boolean value to reflect visibility. If false, it is private.
}
subscription/{viewer's uid}: {
'subscription': Array of uids of the users the viewer is subscribing.
}
So, for the private documents, the basic idea is to look at the viewer's document in the subscription collection, get the data, and check whether the uid of the writer is included there. This will require some Javascript codes, but I don't know what the syntax would be in Firestore security rules, or is possible or not there to begin with.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function publicOrSubscribed() {
return request.auth.uid!=null&&(resource.data.public==true|| /*What could be the syntax here??*/)
}
match /posts/{postid} {
allow read: if publicOrSubscribed();
allow create: if request.auth.uid!=null;
allow update, delete: if request.auth.uid==resource.data.userid;
}
}
}
Any suggestions? If it is not possible in security rules, what might be the workaround? Thanks in advance!

Firestore Match Rules for looking up data in a document that is in another collection

I am having an issue with Firestore rules when the permission is stored in another document in another collection. I haven't been able to find any examples of this, but I have read that it can be done.
I want to do it this way to avoid having to do a lot of writes when a student shares his homework list with many other students. Yes, I know this counts as another read.
I have 3 collections, users, permissions, and homework along with some sample data.
users
{
id: fK3ddutEpD2qQqRMXNW,
name: "Steven Smith"
},
{
id: YI2Fx656kkkk25,
name: "Becky Kinsley"
},
{
id: CAFDSDFR244bb,
name: "Tonya Benz"
}
permissions
{
id: fK3ddutEpD2qQqRMXNW,
followers: [YI2Fx656kkkk25,CAFDSDFR244bb]
}
homework
{
id: adfsajkfsk4444,
owner: fK3ddutEpD2qQqRMXNW,
name: "Math Homework",
isDone: false
}
The start of my firestore rules:
service cloud.firestore {
//lock down the entire firestore then open rules up.
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if false;
}
match /homework/{ } {
allow get: if isSignedIn()
}
// helper functions
function isSignedIn() {
return request.auth != null;
}
function isUser(userId) {
return request.auth.uid == userId;
}
function isOwner(userId) {
return request.auth.uid == resource.data.uid;
}
}
}
Use case:
Steven Smith Shared his homework list with Tonya Benz.
Tonya Benz is logged into the app to view her friend Steven's homework. The app runs this query to get the homework list of Steven Smith.
var homeworkRef = db.collection("homework");
var query = homeworkRef.where("owner", "==", "fK3ddutEpD2qQqRMXNW");
Question:
What is the proper Firestore match rule that takes the "owner" field from the homework collection to look up it up as the id in the permissions collection when the user Tonya Benz is signed in so this query can run.
With your current query and database structure, you won't be able to achieve your goal using security rules.
Firstly, it sounds like you're expecting to be able to filter the results of the query based on the contents of another document. Security rules can't act as query filters. All the documents matched by the query must be granted read access by security rules, or the entire query is denied. You will need to come up with a query that is specific about which documents should be allowed access. Unfortunately, there is no single query that can do this with your current structure, because that would require a sort of "join" between permissions and homework. But Firestore (like all NoSQL databases), do not support joins.
You will need to model your data in such a way that is compatible with rules. You have one option that I can think of.
You could store the list users who should have read have access to a particular document in homework, within that same document, represented as a list field. The query could specify a filter based on the user's uid presence in that list field. And the rule could specify that read access only be granted to users whose IDs are present in that list.
{
id: adfsajkfsk4444,
owner: fK3ddutEpD2qQqRMXNW,
name: "Math Homework",
isDone: false,
readers: [ 'list', 'of', 'userids' ] // filter against this list field
}
The bottom line here is that you'll need to satisfy these two requirements:
Your query needs to be specific about exactly which documents that it expects to be readable. You can't use a rule to filter the results.
Your rule needs a way to determine, using nothing more complicated than the fields of the document itself, or a get() on other known documents, what the access should be for the current uid.

How to get documents which is readable by user on Firestore Cloud depends on rules

I have a basic rule on my Firestore Cloud database. I want to give user cafes that user is in but i am always getting the error "Missing or insufficient permissions.". What is the thing that I miss?
service cloud.firestore {
match /databases/{db}/documents {
match /cafes{
allow read: if request.auth.uid != null;
match /{cafe}{
allow read: if get(/databases/$(db)/documents/cafes/$(cafe)/participants/$(request.auth.uid)) != null;
}
}
}
}
here is the code that i use to reach to documents
export class MyApp {
private rootPage:any;
// rootPage:any = HomePage;
constructor(platform: Platform, statusBar: StatusBar, splashScreen: SplashScreen, afAuth: AngularFireAuth, afs: AngularFirestore) {
platform.ready().then(() => {
statusBar.styleDefault();
splashScreen.hide();
afAuth.auth.onIdTokenChanged(user=>{
if(user){
console.log(user.uid)
afs.collection('cafes').valueChanges().subscribe(console.log)
} else {
console.log('logged out')
}
})
});
}
}
One of the key sentences from the docs on security rules is
Every database request from a Cloud Firestore mobile/web client
library is evaluated against your security rules before reading or
writing any data.
and this should answer your question: You cannot query documents by their visibility for a specific user because this would require Firestore to read and evaluate every document.
If you read this rule
match /{cafe}{
allow read: if get(/databases/$(db)/documents/cafes/$(cafe)/participants/$(request.auth.uid)) != null;
}
as "Allow every user to view a cafe document if it contains his user id in the participants list" it becomes clear that Firestore needs to read the document to answer the question.
What you can try is allow read on all cafe documents and then query for those "the user is in" with the implication that users can read the other cafes as well.
You should take a look at this official guide for a concept on role based access on Firestore documents.
Update
I want to be more precise on the statement "You cannot query documents by their visibility for a specific user because this would require Firestore to read and evaluate every document."
You cannot query for all documents and expect to get those visible to the requesting user (like you do in afs.collection('cafes'), i. e. security rules are not filters.
What you can do is qualify (add a where clause) your query to only include the documents visible to the user. Firestore will match the qualified query against your security rules and if both match, fetch the desired results.

Custom security rules for Cloud Firestore

I want to create a Cloud Firestore realtime database containing groups which users can join and share information to other members of their group. I want to preserve users anonymity so the way I see it to be implemented is:
Group creator generates a group key in format XXXX-XXXX-XXXX-XXXX
Those who want to join must have the group key which they enter in the app, after that they should be able to read, create and update data in that group
So basically the data structure is something like this:
/groups/ : [
//groups as documents
"ABCD-0000-0000-0001" : { /*group data model*/ }
"ABCD-0000-0000-0002" : { /*group data model*/ }
"ABCD-0000-0000-0003" : { /*group data model*/ }
]
The question is, what security rules should I write to permit users to read, create and update data ONLY in the group they belong to (have its group key)? At the same time, how to forbid users from finding out other groups' keys?
Your group structure can remain as is-
groups (Collection) : [
//groups as documents
"ABCD-0000-0000-0001" : { /*group data model*/ }
"ABCD-0000-0000-0002" : { /*group data model*/ }
"ABCD-0000-0000-0003" : { /*group data model*/ } ]
And to maintain the access, you can have another separate collection named group_users as-
group_users(Collection)/ <group_id>(Document)/ members(Collection)/ :
uid_1 (document)
uid_2 (document)
...
Now the rule to allow can be like-
service cloud.firestore {
match /databases/{database}/documents {
match /groups/{group_id} {
allow read, create, update: if exists(/databases/$(database)/documents/group_users/$(group_id)/members/$(request.auth.uid));
}
}
}
When a member joins the group, you will have to add the user's uid to that new collection-
group_users/<group_id>/members
For admins, you can have a similar collection, and uid will be added when the admin creates the group-
group_users/<group_id>/admins
Instead of having a separate collection outside of groups collection, you could have a collection within group itself as an alternative solition, then you will have to modify the group data model to some more extent.
Also FYI, each exists () call in security rules is billed (probably as one read operation).
And here is a detailed documentation explaining almost every possible aspect of firestore security rules.
You can save the group ID in the user's profile. Then create a rule which only allows CRU permissions if that group ID exists.
db.collection.('users').doc({userId}.update({
ABCD-0000-0000-0001: true
})
match /groups/{groupId} {
allow read, create, update: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.$(groupId) == true;
}

Firestore: remove sensitive fields on documents

I'm trying to figure it out how to remove a sensitive field on a firestore document. For example, my collection is a group information. The group is protected with a pin code field. Any one wants to join the group has to know the pin code.
In the meantime, I want to let users query what group is available to join. For query part, I don't want return group information with pin code information. Do we have anyway to remove sensitive fields from a document for Firestore for reading event?
Cloud function only supports write event. 1 possible solution is use cloud function on write event, and put pin code in a separate document. Is there a better solution? THanks.
My group schema is:
group: {
name: string,
pinCode: string
}
A user can either access a document, or they can't. There is no property-level access control in Firestore.
So to accomplish what you want, you will need to store the public and private information in separate documents.
You could either create a second document with the private information in the same collection and then secure them using:
match /databases/{database}/documents {
match /groups/{group} {
allow read: if resource.data.visibility != "private"
}
}
Alternatively (and simpler to secure) you could create a separate collection for the private documents.
You can create a Firebase Function that returns only the fields that you need (non sensitive), here an example:
exports.getTopUsers = functions.https.onCall(async (data) => {
const users = [];
return db.collection('users').orderBy('bids').limit(data.limit).get()
.then((querySnapshot) => {
querySnapshot.forEach((user) => {
users.push({
diplayName: user.get('displayName'),
});
});
return {
topUsers: users,
};
})
.catch((err) => {
console.error(err);
});
});
So, you need to create a separate array (that will be returned) and filling it with only the field that you want while iterating your Firestore collection.

Resources