How to validate Firestore LIST request in Firestore Security Rules when query filter includes array value? - google-cloud-firestore

I have Firestore project setup with standard ACL permissions.
The user collection has docs with roles array [ ...roles ] that defines which roles that user is in
user.roles = [ role1, role2 ... ]
Each document in other collections contains an ACL array that contains a list of roles allowed to access that document
doc.acl = [ role2, role3, ... ]
Because Firestore rules are not filters, we must pass a list of the users roles to the query when requesting docs in a collection. No problem ....
db
.collection('docs')
.where('acl', 'array-contains-any', user.roles) // user.roles = ['role1', 'role2']
Based on this, I should get a result-set that includes only docs that the user is allowed to read.
However, enforcing the security seems to be not possible!
In theory, I should be able to setup a rule like this:
function userHasPermission () {
// here we should check the contents of the *where* query filter, accessible at *resource.data*
let filter = debug(resource.data.acl);
// ... but now we have a problem, see below
}
match /docs/{id} {
allow list: if userHasPermission();
}
In #userHasPermission, we must get the value of the resource.data.acl and if we look at the debug statement, we see the following:
map_value {
fields {
key: "acl"
value {
constraint_value {
simple_constraints {
comparator: LIST_CONTAINS
value {
string_value: "role1"
}
}
}
}
}
}
I can see a map_value. So from this map I can see my key acl.
But if I look deeper into this object, I can see that value of the resource.data.acl is NOT an array, it's a constraint_value of type simple_constraints ... therefore I cannot use the IN keyword, or the LIST methods hasAny and hasAll. And worse, only the first role I passed shows as visible in the debugging statement.
Also it seems not possible to extract the value from the simple_constrains/constraint_value.
Furthermore, only the FIRST value in my array shows up in the constraint!
So it appears that simple ACL strategies are NOT in fact possible with Firestore Security Rules.
Can anyone share an approach that works or a workaround or comment further? There just seems to be no way to extract the LIST (array) from the value sent in the query and therefore I cannot validate the LIST query and therefore cannot implement BASIC ACL permissions!
PS - from the emulator console, here is what shows up in resource tab on the right side: (* pls ignore the fact that in this screen the query key is _accounts and not acl)

Related

In Postgres - How to Remove an Element from JSONB Array (JSONB[]) by Identifier/value

In a Postgresql (13+), a users table has a JSONB[] column (notifications) containing user notification. Notifications are populated by a simple dictionary, resulting in the following array:
[
{
"notification_id": "c20da296", # abbreviating UUID4
"level": "info",
"timestamp": ...,
"content": ...
},
.. more notifications ..
]
When a user dismisses a notification, i'd like to remove it from the array, using the notification's id (while leaving the other notifications). Many resources show how to deal with a JSONB nested structure (e.g., here) or across multiple users (e.g., here). Is there a way to achieve this, maybe using the - operator in some way? I'm expecting something along the following:
UPDATE users SET notifications - `{"notification_id": "c20da296"}`
WHERE user_id = "<user_uuid4>";
note: the user's identifier is known, there is no need to traverse all users to select for the relevant notification_id.
Use jsonb_path_query_array function with a "not equal" condition in the path.
It returns all JSON items returned by the JSON path for the specified JSON value, as a JSON array
Here's an example
UPDATE users
SET notifications = jsonb_path_query_array(notifications, '$ ? (#.notification_id != "<notification_id_for_removal>")')
WHERE user_id = "<user_uuid4>";

How to find and return a specific field from a Mongo collection?

Although I think it is a general question, I could not find a solution that matches my needs.
I have 2 Mongo collections. The 'users' collection and the second one 'dbInfos'.
Now, I have a template called 'Infos' and want the already existing fields in the Mongo collections to be presented to the user in input fields in case there is data in the collection. When no data is provided in the database yet, it should be empty.
So here is my code, which works fine until I want to capture the fields from the second collection.
Template.Infos.onRendered(function() {
$('#txtName').val(Meteor.user().profile.name);
$('#txtEmail').val(Meteor.user().emails[0].address);
});
These 2 work great.
But I don´t know how to query the infos from the collection 'dbInfos', which is not the 'users' collection. Obviously Meteor.user().country does not work, because it is not in the 'users' collection. Maybe a find({}) query? However, I don´t know how to write it.
$('#txtCountry').val( ***query function***);
Regarding the structure of 'dbInfos': Every object has an _id which is equal to the userId plus more fields like country, city etc...
{
"_id": "12345",
"country": "countryX",
"city": "cityY"
}
Additionally, how can I guarantee that nothing is presented, when the field in the collection is empty? Or is this automatic, because it will just return an empty field?
Edit
I now tried this:
dbInfos.find({},{'country': 1, '_id': 0})
I think this is the correct syntax to retrieve the country field and suppress the output of the _id field. But I only get [object Object] as a return.
you're missing the idea of a foreign key. each item in a collection needs a unique key, assigned by mongo (usually). so the key of your country info being the same as the userId is not correct, but you're close. instead, you can reference the userId like this:
{
"_id": "abc123",
"userId": "12345",
"country": "countryX",
"city": "cityY"
}
here, "abc123" is unique to that collection and assigned by mongo, and "12345" is the _id of some record in Meteor.users.
so you can find it like this (this would be on the client, and you would have already subscribed to DBInfos collection):
let userId = Meteor.userId();
let matchingInfos = DBInfos.find({userId: userId});
the first userId is the name of the field in the collection, the second is the local variable that came from the logged in user.
update:
ok, i think i see where you're getting tripped it. there's a difference between find() and findOne().
find() returns a cursor, and that might be where you're getting your [object object]. findOne() returns an actual object.
for both, the first argument is a filter, and the second argument is an options field. e.g.
let cursor = DBInfos.find({
userId: Meteor.userId()
},
{
fields: {
country: 1
}
});
this is going to:
find all records that belong to the logged in user
make only the country and _id fields available
make that data available in the form of a cursor
the cursor allows you to iterate over the results, but it is not a JSON object of your results. a cursor is handy if you want to use "{{#each}}" in the HTML, for example.
if you simply change the find() to a findOne():
let result = DBInfos.findOne({ /** and the rest **/
... now you actually have a JSON result object.
you can also do a combination of find/fetch, which works like a findOne():
let result = DBInfos.find({
userId: Meteor.userId()
},
{
fields: {
country: 1
}
}).fetch();
with that result, you can now get country:
let country = result.country;
btw, you don't need to use the options to get country. i've been assuming all this code is on the client (might be a bad assumption). so this will work to get the country as well:
let result = DBInfos.findOne({userId: Meteor.userId()});
let country = result.country;
what's going on here? it's just like above, but the result JSON might have more fields in it than just country and _id. (it depends on what was published).
i'll typically use the options field when doing a find() on the server, to limit what's being published to the client. on the client, if you just need to grab the country field, you don't really need to specify the options in that way.
in that options, you can also do things like sort the results. that can be handy on the client when you're going to iterate on a cursor and you want the results displayed in a certain order.
does all that make sense? is that what was tripping you up?

mongo find all with field that is object having a specified key

A mongo db has documents that look like:
{
"_id": : ObjectId("55cb43e8c78b04f43f2eb503"),
<some fields>
"topics": {
"test/23/result": 149823788,
"test/27/result": 147862733,
"input/misc/test": 14672882
}
}
I need to find all documents that have a topics field that contains a particular key. i.e. find all documents that have a topics.key = "test/27/result"
I've tried a number of things but none work yet, neither attempt below work,
they return no records event though some should match:
db.collName.find({"topics.test/27/result": {$exists:true}});
db.collName.find({"topics.test\/27\/result": {$exists:true}});
How can I make the query work?
The slash characters are inserted by another process. They are mqtt topic names.
I found the solution to my problem:
I was building the query wrong in my code. In the example below, evtData.source contains the key name to search for, i.e. 'test/27/result'
The query methodology that works for me is:
var query = {};
query['topics.' + evtData.source] = {$exists: true};
db.collName.find(query)

Build a reactive publication with additional fields in each document

I want to make a publication with several additional fields, but I don't want to either use Collection.aggregate and lose my publication updates when the collection change (so I can't just use self.added in it either).
I plan to use Cursor.observeChanges in order to achieve that. I have two major constraints:
I don't want to publish all the documents fields
I want to use some of the unpublished fields to create new ones. For example, I have a field item where I store an array of item _id. I don't want to publish it, but I want to publish a item_count field with the length of my field array
Here comes the approach:
I plan to chain find queries. I never did that so I wonder if it possible. The general (simplified) query structure would be like this: http://jsfiddle.net/Billybobbonnet/1cgrqouj/ (I cant get the code properly displayed here)
Based on the count example in Meteor documentation, I store my query in a variable handle in order to stop the changes notification if a client unsubscribes:
self.onStop(function () {
handle.stop();
});
I add a flag initializing = true; before my query and I set it to true just before calling self.ready();. I use this flag to change my itemCount variable only if it is the publication is initialized. So basically, I change my switch like that:
switch (field) {
case "item"
if (!initializing)
itemCount = raw_document.item.length;
break;
default:
}
I wanted to check that this approach is good and possible before committing into big changes in my code. Can someone confirm me if this is the right way to go?
It's relatively easy to keep fields private even if they are part of the database query. The last argument to self.added is the object being passed to the client, so you can strip/modify/delete fields you are sending to the client.
Here's a modified version of your fiddle. This should do what you are asking for. (To be honest I'm not sure why you had anything chained after the observeChanges function in your fiddle, so maybe I'm misunderstanding you, but looking at the rest of your question this should be it. Sorry if I got it wrong.)
var self = this;
// Modify the document we are sending to the client.
function filter(doc) {
var length = doc.item.length;
// White list the fields you want to publish.
var docToPublish = _.pick(doc, [
'someOtherField'
]);
// Add your custom fields.
docToPublish.itemLength = length;
return docToPublish;
}
var handle = myCollection.find({}, {fields: {item:1, someOtherField:1}})
// Use observe since it gives us the the old and new document when something is changing.
// If this becomes a performance issue then consider using observeChanges,
// but its usually a lot simpler to use observe in cases like this.
.observe({
added: function(doc) {
self.added("myCollection", doc._id, filter(doc));
},
changed: function(newDocument, oldDocument)
// When the item count is changing, send update to client.
if (newDocument.item.length !== oldDocument.item.length)
self.changed("myCollection", newDocument._id, filter(newDocument));
},
removed: function(doc) {
self.removed("myCollection", doc._id);
});
self.ready();
self.onStop(function () {
handle.stop();
});
To solve your first problem, you need to tell MongoDB what fields it should return in the cursor. Leave out the fields you don't want:
MyCollection.find({}, {fields: {'a_field':1}});
Solving your second problem is also pretty easy, I would suggest using the collection helpers packages. You could accomplish this easily, like so:
// Add calculated fields to MyCollection.
MyCollection.helpers({
item_count: function() {
return this.items.length;
}
});
This will be run before an object is added to a cursor, and will create properties on the returned objects that are calculated dynamically, not stored in MongoDB.

How to I ensure that all items in an $in clause return a match in MongoDB?

I am working with a set of documents like this:
{
name : "BCC 204",
//etc
}
I have a list of names that I want to map to their DB entries.
For example:
var names = [ "BCC 204", "STEW 101", "SMTH 123" ]
and I want to make a query like this
db.labs.find( { name : { $in: names } } );
But the $in operator does not ensure that each item in the names array matches a result in the db.
(More info, names are unique)
You can't do this in the query. $in will check that a document matches at least one entry in the array given, but it's not going to consider the entire result set. This is a concern you'll need to manage in your application. Given a list of inputs, you will need to retrieve your results then check that given_names - results.map(:name) is empty.
To put it more simply, queries match documents, which compose a result set - they don't match a result set.