onSnapshot permission error when matching custom claim to Firestore wildcard - javascript

Im trying to allow the document reading for only users who have in their custom claim the "houseId" that is identical to the document id.
This is the Firestore rule that is not working:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /houses/{houseId} {
allow read: if request.auth.token.houseId == houseId;
}
}
}
I also try the following code, but it didn't work either:
match /houses/{houseId} {
allow read: if request.auth.token.houseId == resource.data.houseId;
}
Instead, if I use the "string" version of the houseId it works.
match /houses/{houseId} {
allow read: if request.auth.token.houseId == 'T45bpx2wzskdtxoowfIw';
}
My client side code for fetching the data is (the inCharge field of the house document is just for querying the specific document where the user has been added):
useEffect(() => {
const subscriber = db.collection('houses')
.where('inCharge.inChargeId', '==', user.userId)
.onSnapshot(querySnapshot => {
const houses = [];
querySnapshot.forEach(documentSnapshot => {
houses.push({
...documentSnapshot.data(),
houseId: documentSnapshot.id
});
});
dispatch(housesActions.setHouses(houses));
setIsLoading(false);
});
return () => subscriber();
}, [])
My user claims are for example like this:
User1: {role: 'inCharge', houseId: T45bpx2wzskdtxoowfIw}
user2: {role: 'viewer', houseId: T45bpx2wzskdtxoowfIw}
...
The documents are like this:
houseId:
name: 'Name of the house'
createdAt: timestamp of creation
inCharge: {
name: 'name of the inCharge person'
inChargeId: 'id of the inCharge person'
}
otherFIelds:
...

Your query does not work because Firestore security rules are not filters. Be sure to read that documentation, and this blog.
It seems that you're depending on the rules to filter out documents that match the user's custom claims. This isn't going to work. Your query needs to apply the filter, and the rules can just check to make sure that the filter is correct.
In your specific case, it looks like each user can access no more than exactly one document in the houses collection, identified by the houseId in their custom claims. Since that's the case, the best thing you can do here is simply get() that document by its ID instead of querying using the other filter that you have now. You can then check that document to see if it contains the values you expect.
You can also try to use the same query you have now, but filter the documentId for the one you provide, which must match the one in custom claims:
.where(firebase.FieldValue.documentId(), '==', houseId)
But this seems more complicated to me since your rules require that each user can only read their one document.

Related

Populate field then find - mongoose

I have a Cheques and a Payees collection, every cheque has its corresponding Payee ID.
What I'm trying to do is to write some queries on cheques, but I need to preform the searching after populating the payee (to get the name)
const search = req.query.search || "";
const cheques = await Cheque
.find({
isCancelled: false,
dueDate: { $gte: sinceDate, $lte: tillDate }
})
.select("_id serial dueDate value payee")
.skip(page * limit)
.limit(limit)
.sort({ dueDate: -1, serial: 1 })
.populate({
path: "payee",
select: "name"
})
I guess what I'm trying do is fit this somewhere in my code,
match: {
name: { $regex: search, $options: "i" }
},
I have tried to put the match within the populate, but then it will still find all cheques even if they don't satisfy the population match but populate as null.
I hate this answer and I hope someone is going to post a better one, but I've surfed the web for that with no luck.
The only method I was able to find is to use the $lookup method in aggregation.
So you'll have to change your code from calling .find() to .aggregate().
It's not the sad news, it's great, stable and no problems at all.
but I hated that because it's going to change some patterns you might be following in your code.
const search = req.query.search || "";
const cheques = await Cheque
.aggregate([
{
$lookup: { // similar to .populate() in mongoose
from: 'payees', // the other collection name
localField: 'payee', // the field referencing the other collection in the curent collection
foreignField: '_id', // the name of the column where the cell in the current collection can be found in the other collection
as: 'payee' // the field you want to place the db response in. this will overwrite payee id with the actual document in the response (it only writes to the response, not on the database, no worries)
},
{ // this is where you'll place your filter object you used to place inside .find()
$match: {
isCancelled: false,
dueDate: { $gte: sinceDate, $lte: tillDate }
'payee.branch': 'YOUR_FILTER', // this is how you access the actual object from the other collection after population, using the dot notation but inside a string.
}
},
{ // this is similar to .select()
$project: {_id: 1, serial: 1, dueDate: 1, value: 1, payee: 1}
},
{
$unwind: '$payee' // this picks the only object in the field payee: [ { payeeDoc } ] --> { payeeDoc }
}
])
.skip(page * limit)
.limit(limit)
.sort({ dueDate: -1, serial: 1 })
Notice how you can no longer chain .select() and .populate() on the model query the way you used to do it on .find(), because now you're using .aggregate() which returns a different class instance in mongoose.
you can call .projcet() instead of doing it inside the aggregation array if you want to, but as far as I know, you can't use the .select() method.
My opinion-based solution to this problem is to include the payee information you need for filtering in the Cheque collection.
In my senario, this happen when I was filtering for the sake of my users-roles and permissions, so someone can not see what another one is seeing.
It's up to you, but this makes it easier later when you want to generate the reports (I assume you're working on a payment service).
The populate() feature provided by mongoose first fetches all the Cheques with given conditions and then makes another query to payee with _ids to populate the fields you wanted.
https://mongoosejs.com/docs/api.html#query_Query-populate
by putting match in populate you're filtering which cheques need to be populated but not the cheques themselves.
A simple solution for this is to filter the cheques which are populated as null and return them for your use.
If you see more queries of this sort and/or the collection is huge, it's better you add the payee name in the Cheques collection itself if that fits your purpose.

Pass query criteria to mongoDB aggregation

our current setup is: SPA frontend, Azure functions with mongoose middleware, MongoDB
(Maybe first read the question***)
Since we have a lot of documents in our DB and our customer wants to query them we are facing the following problem:
The user is assigned to his organization. He wants to search for Doc1s he has not responded to.
Doc1
{
_id
organization -> partitionKey
content
}
By creating doc2 with reference to doc1 he can respond.
Doc2
{
_id
organization -> partitionKey
Doc1ref
content
}
We have a 1:n relationship.
At the moment we filter just by query criteria of doc1 with limit and skip options.
But the new requirement is to filter the same way by referring doc2s.
I was thinking of:
Doing it in my code => Problem: after we have read with limit=100 and I filter it by my code, the result is not 100 anymore.
Extending doc1 by doc2 arrays => Must be the last option
Dynamic aggregation, Prepared in the code and executed at runtime => Don't want to user dynamic aggregations and the benefits of mongoose are almost lost.
Create a MongoDB view with lookup aggregation (populating doc1 by doc1.respondedOrganizations) => Problem is see here is the performance. When searching a lot of documents and then joining them by a non partitionKey.
*** So, I come to my question:
Is it possible to pass a virtual (not existing) query criteria...
doc1.find({ alreadyResponded : my.organization } )
...and use it as input variable in an aggregation
{
$lookup: {
from: Doc2s,
localField: _id,
foreignField: Doc1ref,
as: < output array field >
pipeline: [{
$match: {
$organization: {
$eq: $$alreadyResponded
}]
}
}
It would reduce query performance extremly.
Thanks

How to read all nested collections of all users on firestore? [duplicate]

I thought I read that you can query subcollections with the new Firebase Firestore, but I don't see any examples. For example I have my Firestore setup in the following way:
Dances [collection]
danceName
Songs [collection]
songName
How would I be able to query "Find all dances where songName == 'X'"
Update 2019-05-07
Today we released collection group queries, and these allow you to query across subcollections.
So, for example in the web SDK:
db.collectionGroup('Songs')
.where('songName', '==', 'X')
.get()
This would match documents in any collection where the last part of the collection path is 'Songs'.
Your original question was about finding dances where songName == 'X', and this still isn't possible directly, however, for each Song that matched you can load its parent.
Original answer
This is a feature which does not yet exist. It's called a "collection group query" and would allow you query all songs regardless of which dance contained them. This is something we intend to support but don't have a concrete timeline on when it's coming.
The alternative structure at this point is to make songs a top-level collection and make which dance the song is a part of a property of the song.
UPDATE
Now Firestore supports array-contains
Having these documents
{danceName: 'Danca name 1', songName: ['Title1','Title2']}
{danceName: 'Danca name 2', songName: ['Title3']}
do it this way
collection("Dances")
.where("songName", "array-contains", "Title1")
.get()...
#Nelson.b.austin Since firestore does not have that yet, I suggest you to have a flat structure, meaning:
Dances = {
danceName: 'Dance name 1',
songName_Title1: true,
songName_Title2: true,
songName_Title3: false
}
Having it in that way, you can get it done:
var songTitle = 'Title1';
var dances = db.collection("Dances");
var query = dances.where("songName_"+songTitle, "==", true);
I hope this helps.
UPDATE 2019
Firestore have released Collection Group Queries. See Gil's answer above or the official Collection Group Query Documentation
Previous Answer
As stated by Gil Gilbert, it seems as if collection group queries is currently in the works. In the mean time it is probably better to use root level collections and just link between these collection using the document UID's.
For those who don't already know, Jeff Delaney has some incredible guides and resources for anyone working with Firebase (and Angular) on AngularFirebase.
Firestore NoSQL Relational Data Modeling - Here he breaks down the basics of NoSQL and Firestore DB structuring
Advanced Data Modeling With Firestore by Example - These are more advanced techniques to keep in the back of your mind. A great read for those wanting to take their Firestore skills to the next level
What if you store songs as an object instead of as a collection? Each dance as, with songs as a field: type Object (not a collection)
{
danceName: "My Dance",
songs: {
"aNameOfASong": true,
"aNameOfAnotherSong": true,
}
}
then you could query for all dances with aNameOfASong:
db.collection('Dances')
.where('songs.aNameOfASong', '==', true)
.get()
.then(function(querySnapshot) {
querySnapshot.forEach(function(doc) {
console.log(doc.id, " => ", doc.data());
});
})
.catch(function(error) {
console.log("Error getting documents: ", error);
});
NEW UPDATE July 8, 2019:
db.collectionGroup('Songs')
.where('songName', isEqualTo:'X')
.get()
I have found a solution.
Please check this.
var museums = Firestore.instance.collectionGroup('Songs').where('songName', isEqualTo: "X");
museums.getDocuments().then((querySnapshot) {
setState(() {
songCounts= querySnapshot.documents.length.toString();
});
});
And then you can see Data, Rules, Indexes, Usage tabs in your cloud firestore from console.firebase.google.com.
Finally, you should set indexes in the indexes tab.
Fill in collection ID and some field value here.
Then Select the collection group option.
Enjoy it. Thanks
You can always search like this:-
this.key$ = new BehaviorSubject(null);
return this.key$.switchMap(key =>
this.angFirestore
.collection("dances").doc("danceName").collections("songs", ref =>
ref
.where("songName", "==", X)
)
.snapshotChanges()
.map(actions => {
if (actions.toString()) {
return actions.map(a => {
const data = a.payload.doc.data() as Dance;
const id = a.payload.doc.id;
return { id, ...data };
});
} else {
return false;
}
})
);
Query limitations
Cloud Firestore does not support the following types of queries:
Queries with range filters on different fields.
Single queries across multiple collections or subcollections. Each query runs against a single collection of documents. For more
information about how your data structure affects your queries, see
Choose a Data Structure.
Logical OR queries. In this case, you should create a separate query for each OR condition and merge the query results in your app.
Queries with a != clause. In this case, you should split the query into a greater-than query and a less-than query. For example, although
the query clause where("age", "!=", "30") is not supported, you can
get the same result set by combining two queries, one with the clause
where("age", "<", "30") and one with the clause where("age", ">", 30).
I'm working with Observables here and the AngularFire wrapper but here's how I managed to do that.
It's kind of crazy, I'm still learning about observables and I possibly overdid it. But it was a nice exercise.
Some explanation (not an RxJS expert):
songId$ is an observable that will emit ids
dance$ is an observable that reads that id and then gets only the first value.
it then queries the collectionGroup of all songs to find all instances of it.
Based on the instances it traverses to the parent Dances and get their ids.
Now that we have all the Dance ids we need to query them to get their data. But I wanted it to perform well so instead of querying one by one I batch them in buckets of 10 (the maximum angular will take for an in query.
We end up with N buckets and need to do N queries on firestore to get their values.
once we do the queries on firestore we still need to actually parse the data from that.
and finally we can merge all the query results to get a single array with all the Dances in it.
type Song = {id: string, name: string};
type Dance = {id: string, name: string, songs: Song[]};
const songId$: Observable<Song> = new Observable();
const dance$ = songId$.pipe(
take(1), // Only take 1 song name
switchMap( v =>
// Query across collectionGroup to get all instances.
this.db.collectionGroup('songs', ref =>
ref.where('id', '==', v.id)).get()
),
switchMap( v => {
// map the Song to the parent Dance, return the Dance ids
const obs: string[] = [];
v.docs.forEach(docRef => {
// We invoke parent twice to go from doc->collection->doc
obs.push(docRef.ref.parent.parent.id);
});
// Because we return an array here this one emit becomes N
return obs;
}),
// Firebase IN support up to 10 values so we partition the data to query the Dances
bufferCount(10),
mergeMap( v => { // query every partition in parallel
return this.db.collection('dances', ref => {
return ref.where( firebase.firestore.FieldPath.documentId(), 'in', v);
}).get();
}),
switchMap( v => {
// Almost there now just need to extract the data from the QuerySnapshots
const obs: Dance[] = [];
v.docs.forEach(docRef => {
obs.push({
...docRef.data(),
id: docRef.id
} as Dance);
});
return of(obs);
}),
// And finally we reduce the docs fetched into a single array.
reduce((acc, value) => acc.concat(value), []),
);
const parentDances = await dance$.toPromise();
I copy pasted my code and changed the variable names to yours, not sure if there are any errors, but it worked fine for me. Let me know if you find any errors or can suggest a better way to test it with maybe some mock firestore.
var songs = []
db.collection('Dances')
.where('songs.aNameOfASong', '==', true)
.get()
.then(function(querySnapshot) {
var songLength = querySnapshot.size
var i=0;
querySnapshot.forEach(function(doc) {
songs.push(doc.data())
i ++;
if(songLength===i){
console.log(songs
}
console.log(doc.id, " => ", doc.data());
});
})
.catch(function(error) {
console.log("Error getting documents: ", error);
});
It could be better to use a flat data structure.
The docs specify the pros and cons of different data structures on this page.
Specifically about the limitations of structures with sub-collections:
You can't easily delete subcollections, or perform compound queries across subcollections.
Contrasted with the purported advantages of a flat data structure:
Root-level collections offer the most flexibility and scalability, along with powerful querying within each collection.

Contentful JS: Unable to retrieve entries by matching Reference field

I am using Contentful's Javascript API to build a project. Currently, I'm having an issue where I get "undefined" as a return for the following call.
const query = {
content_type: "vehicle",
include: 2,
select: "fields",
"fields.site.fields.siteName[match]": id
};
I've set up "vehicle" as a model which uses a "site" reference with names, addresses and so forth. It seems to be possible to use [exist] on the first level, like
"fields.site[exists]": true
which works, but is unsatisfactory for what I need.
What I need are any Vehicles that belong to a named Site. Obviously, I've made sure to add the relevant content, and I can indeed see the data when omitting the "fields.site.fields..." line. For security purposes, I would very much not have vehicles for other sites showing up in the response.
Am I missing something? Upping the "include" level does not do anything either.
👋🏻
I believed you didn't see one sentence in the docs.
Second is fields.brand.sys.contentType.sys.id=sFzTZbSuM8coEwygeUYes which you use to to filter on fields of entries from content type 2PqfXUJwE8qSYKuM0U6w8M.
So basically to make your query work you have also to define the content type of the entry your search query is matching against.
I quickly prototyped your case in Node.js.
const { createClient } = require('contentful');
const client = createClient({
space: '...',
accessToken: '...'
});
client.getEntries({
content_type: 'vehicle',
select: 'fields',
// this is the line you're missing
'fields.site.sys.contentType.sys.id': 'site',
'fields.site.fields.siteName': 'vw-site'
}).then(({items}) => {
console.log(items);
}).catch(e => console.log(e));
You can find a detailed example in the docs
Hope that helps :)

Meteor mongo: sync fields across collections

I have a meeting document like this:
{
"name":"Meeting Name",
"uuid":"NYoc2aL6",
"participants":[
{
"id":"JLKGZnfFkGvX9DHgz",
"status":"joined",
"name":"Guest 03"
},
{
// newly invited user, user hasn't logged in with invite url yet
"id":"",
"status":"invited",
"name":"email#email.com"
}
]
}
and I need to synchronize the 'name' field with the name in the Users collection. Is there an automatic way to do this, like at the database level, or am I stuck with manually updating in every place that the name is changed?
This is a pretty common pattern in Meteor. You can use the matb33:collection-hooks package to "hook" the collection update to synchronize the shared value(s). This should be done server-side of course so you don't have to worry about some of the related documents not being available to you.
Example:
Meteor.users.after.update((userId, doc, fieldNames, modifier, options)=>{
if ( fieldNames.indexOf('profile.name') > -1 ){ // the name was changed
Meetings.update({ 'participants.id': doc._id },
{ $set: { 'participants.name': doc.profile.name }},
{ multi: true });
}
});
There is no "automatic" way to do this that I know of in Mongo or Meteor. However, why not take the common fields out of your document and just link the ID? This is known as "Database Normalization", which is a process by which you remove redundant data from your tables (collections in Mongo) to prevent these sorts of problems.
This could be done 'automatically' by observing changes on the users db:
var usersCursor = Meteor.users.find();
usersCursor.observeChanges({"changed":function(id, fields){
if(fields.profile.name){
... do whatever needs to be done ...
}
}});

Categories

Resources