Firebase firestore rules don't always work on web - google-cloud-firestore

Consider the following rule:
match /triplogs/{anyDocument} {
allow create: if request.auth != null;
allow read, update, delete: if request.auth.uid == resource.data.userId;
}
If I hit that with my logged in user, I get the standard:
Error: FirebaseError: [code=permission-denied]: Missing or insufficient permissions.
A few points to know:
I am indeed logged in, I can output the UID of the current user and it is correct.
I can hit this same route via the mobile version and it works.
If I remove the resource.data.userId check, and just check to see if the user is authenticated (even hard code a uid), it works (so the rule seems to get checked against appropriately)
The simulator says "resources" is null. I don't understand how the resources object could be null, maybe it's something with the simulator?
Any help would be appreciated, I've troubleshot and googled around for the last few hours with no success.
Query in question:
// ... My firebase wrapper:
import { firebaseApp } from '../utils/firebase';
// ...
const collectionName = 'triplogs';
export const fetchTrips = async (filters?: Filter[]) => {
let query;
const ref = db?.collection(collectionName);
const trips: TripLog[] = [];
if (filters) {
filters.forEach((filter) => {
query = ref?.where(filter.field, filter.operator, filter.value);
});
}
try {
const obj = query || ref;
console.log(firebaseApp()?.auth().currentUser?.uid); // <-- this is populated with my logged in UID, FWIW
const docs = await obj?.get();
docs?.forEach((doc) => {
const data = doc.data() as TripLog;
trips.push({
...data,
id: doc.id
});
});
return trips;
} catch (err) {
throw new Error(err);
}
};
Here's the info when I try to run a sample request in the simulator, against those auth rules:
{
"path": "/databases/%28default%29/documents/triplogs/%7BanyDocument%7D"
"method": "get"
"auth": {
"uid": "5ompaySrXQcL9veWr3QlSujwlDS2"
"token": {
"sub": "5ompaySrXQcL9veWr3QlSujwlDS2"
"aud": "....omitted"
"firebase": {
"sign_in_provider": "google.com"
}
"email": "...omitted..."
"email_verified": true
"phone_number": ""
"name": "...."
}
}
"time": "2020-10-30T16:48:39.601Z"
}
The error in the sim is:
Error: simulator.rules line [32], column [58]. Null value error.
^^ Which is the resource object
If I change the auth rules to be the following, then I will get back data
allow read, update, delete: if request.auth.uid != null;
Data like:
[{
"createdAt": 1597527979495,
"name": "Foo Bar Trip",
"date": 1597527979495,
"userId": "5ompaySrXQcL9veWr3QlSujwlDS2",
"id": "FnH2E9WfDkpRHPLXxlDy"
}]
But as soon as I check against a field, the "resources" object is null
allow read, update, delete: if request.auth.uid == resource.data.userId;

I'll note you seem to be querying a collection, but your rules seem written for a document... as stated here , rules are not filters - they do not separate out individual documents in a collection. Are you doing that in your filter[array]?

Don't know what else might be going on, but your processing of the filter Array (I do a similar thing) won't work - you are REPLACING the query on each loop, not extending it. You might try
query = ref;
if (filters) {
filters.forEach((filter) => {
query = query?.where(filter.field, filter.operator, filter.value);
});
}
to extend the query.
Also, unless your wrapper does more than it shows, your line:
const docs = await obj?.get();
"gets" the QuerySnapshot, not the array of docs. You'd have to do
const docs = await obj?.get().docs;
to get the array of actual docs.
Finally, we have no idea (from your above snippets) whether the userID field is even present in your Firestore documents

Solution
I figured it out. I incorrectly assumed the query I was making was auto-filtered by the auth rule (in this case request.auth.id === userId) ... but the auth middleware to firestore doesn't do that. Makes sense. So I just added my filter ... something like this:
fetchTrips([{
field: 'userId',
operator: '==',
value: user.uid,
}]);
This works and returns the correct results. Trying to change the userID correctly throws the auth error too. So that was it.

Related

how to query for key exists in object field in firestore document (firebase web sdk)

I have subjects collection. In this collection every document has tutors field that is object where key is id of tutors( from tutors collection)
tutors: {
"tutor_id_1": {
"name": "jonas",
"email": "jonas#gmail.com"
},
"tutor_id_2":{
"name": "stephen",
"email": "stephen#gmail.com"
},
"tutor_id_3":{
"name": "maria",
"email":"maria#gmail.com"
}
}
So how to query subjects where tutors field contain tutor id equal to "tutor_id_1" ?
I found one way
if I have two variables in client side
const tutorToFindId = "xxx"
const tutorToFindEmail = "YYY"
query(
collection(db, 'subjects'),
where(`tutors.${tutorToFindId}.email`, '==', `${tutorToFindEmail}`)
),
Is there any other way ??
As I understand, "tutor_id_1" is being used as a unique id. Considering that, you may structure your data model with "tutors" as a subcollection instead of a field and you will be able to get the content of that specific document as follows:
const docRef = db.collection('subjects').doc('subjectX').collection('tutors').doc(tutorToFindId);
const tutor = await docRef.get();
if (!tutor.exists) {
console.log('No such document!');
} else {
console.log('Document data:', tutor.data());
}
db.getBy({ where: { [`tutors.${tutorToFindId}.email`]: tutorToFindEmail } });
Take a look at the getBy function in https://www.npmjs.com/package/firebase-firestore-helper
Disclaimer: I am the creator of this library. It helps to manipulate objects in Firebase Firestore (and adds Cache)
Enjoy!

Mongoose: Defining 404 status for not finding a document doesnt work

I,m learning MongoDB and mongoose and now I have a problem in defining a 404 status for my route handler. Here is the code:
app.get('/users/:id', async (req, res) => {
const _id = req.params.id
try {
const user = await User.findById(_id)
if (!user) {
return res.status(404).send()
}
res.send(user)
} catch (error) {
res.status(500).send()
}
})
Now if I give it an id that doesn't exist, it doesn't give me 404 Not Found status. it only executes the catch block which is not what I want.
I would appreciate it if you tell me where I made mistake or tell me a way to get error handling for that.
Thanks
The problem
As you can see in the log
CastError: Cast to ObjectId failed for value "6082d50a2c89db3164" at path "_id" for model "User"
It means : the value you provide to findById function ("6082d50a2c89db3164") is not a valid ObjectId.Then the catch block is executed.
Suggestion
1. Validate the parameter before query in database
I understand that you're trying to provide some id that doesn't exist in the database to test. But IMHO, there a difference between 2 cases :
you provide a valid id, and this id cannot be found in the database. It should return 404 in this case
you provide an invalid id in the request, it could be a string like "6082d50a2c89db3164", or even "#Q*&$(##*" or anything we could imagine. For this case, it could be better if we validate the input (req.params._id) to ensure that the format is valid. The code will be something like this:
app.get('/users/:id', async (req, res) => {
const _id = req.params.id;
// validate params
if(!isValidateObjectId(_id)) { // the function we need to write
res.status(200).send("Invalid params"); // you can define your status and message
return;
}
// good params, get user from database
try {
const user = await User.findById(_id)
if (!user) {
return res.status(404).send()
}
res.send(user)
} catch (error) {
res.status(500).send()
}
})
2. Use findOne() method instead of findById
If you want a simpler solution, don't use findById because the function expects a valid ObjectId. We can use findOne() method :
app.get('/users/:id', async (req, res) => {
const _id = req.params.id
try {
const user = await User.findOne({_id : _id})
if (!user) {
return res.status(404).send()
}
res.send(user)
} catch (error) {
res.status(500).send()
}
})
(IMHO, the first solution is better though..)
Some helpful link :
https://docs.mongodb.com/manual/reference/method/ObjectId/
Can I determine if a string is a MongoDB ObjectID?
https://mongoosejs.com/docs/api.html#model_Model.findOne

Real time database REST API Constraint index field must be a JSON primitive

I'm trying to make a query to the Real-time database from google using their rest API.
this is how the data of "waiters" path looks like :
{
"-Kb2dYPV0yUXpD_1moc9": {
"id": "Ky7gTz0BFyRudih0DhSAe3lwci13",
"role": "normal"
},
"-Kb2etFm1xHd8sSsESeK": {
"id": "VRBNr5OnMQaDoLpkKppapyDW8JZ2",
"role": "admin"
},
}
On the client-side, I know the id and what I would like to get is the role (i.e "admin" or whatever it is) but I don't know the autogenerated location-id ("-Kb2etFm1xHd8sSsESeK").
Does someone know how to proceed?
I tried :
static Future<RoleType> fetchRole(String userId, String token) async {
try {
var url =
"https://xxx.firebaseio.com/waiters.json?orderBy=\"id\"&equalTo=\"" +
"$userId\"" +
"?auth=" +
token;
final response = await http.get(url);
final extractedData = json.decode(response.body);
if (extractedData == null) return null;
return RoleType(extractedData['role']);
} catch (error) {
print("[fetchRole]:: " + error.toString());
throw error;
}
}
the URL without the token gives :
https://xxx.firebaseio.com/waiters.json?
orderBy="id"&equalTo="VRBNr5OnMQaDoLpkKppapyDW8JZ2"?auth=token
But I get this error :
{error: Constraint index field must be a JSON primitive}
I still get this error when I update the rules as follow:
{
"rules": {
".read": "auth != null", // 2020-11-9
".write": "auth != null", // 2020-11-9
"waiters": {
".indexOn": ["id"]
}
}
}
Thanks in advance for your help!
For some reason, it works now. I don't know why... Mysteries of CS.
Hi just got the same error when i was playing with ionic with angular.
It turn out one of my character is not an English character, i typed “” instead of "" in Chinese character.
Hope this help if anyone passing by ;)

How to identify unique users with Diagflow

I am trying to make an assistant app and was using the cloud firestore service of firebase to send the response back to my app using webhook as fulfilment. I have used 'session' parameter in request JSON according to this documentation and sending fulfilmentText as response to the user. But whenever user launches the app, a new session is created which I don't want. I simply want, just a single entry for each user in my database so how to achieve that using dialogflow.
In Alexa Skill, we have deviceId as parameter by which we can uniquely identify the user irrespective of the session id but is there any parameter in the dialogflow request JSON. If not, then how to achieve this task without it.
The request JSON I am getting from Dialogflow has a userID in it, so can I use the userId or should I go with userStorage provided the userStorage parameter is not available in the request JSON.
request.body.originalDetectIntentRequest { source: 'google', version: '2', payload: { surface: { capabilities: [Object] },
inputs: [ [Object] ],
user:
{ locale: 'en-US',
userId: 'ABwppHG5OfRf2qquWWjI-Uy-MwfiE1DQlCCeoDrGhG8b0fHVg7GsPmaKehtxAcP-_ycf_9IQVtUISgfKhZzawL7spA' },
conversation:
{ conversationId: '1528790005269',
type: 'ACTIVE',
conversationToken: '["generate-number-followup"]' },
availableSurfaces: [ [Object] ] } }
EDIT : Thank You #Prisoner for the answer but I am unable to send the random ID generated in the response and set in in the payload. Below is the code where I am generating the uuid and storing it in firestore. What I am doing wrong in the below code due to which new uuid is generated for returning user and therefore response is shown as No document found in the database. I suppose I am not sending uuid appropriately. Please help.
exports.webhook = functions.https.onRequest((request, response) => {
console.log("request.body.queryResult.parameters", request.body.queryResult.parameters);
console.log("request.body.originalDetectIntentRequest.payload", request.body.originalDetectIntentRequest.payload);
let userStorage = request.body.originalDetectIntentRequest.payload.user.userStorage || {};
let userId;
console.log("userStorage", userStorage);
if (userId in userStorage) {
userId = userStorage.userId;
} else {
var uuid = require('uuid/v4');
userId = uuid();
userStorage.userId = userId
}
console.log("userID", userId);
switch (request.body.queryResult.action) {
case 'FeedbackAction': {
let params = request.body.queryResult.parameters;
firestore.collection('users').doc(userId).set(params)
.then(() => {
response.send({
'fulfillmentText' : `Thank You for visiting our ${params.resortLocation} hotel branch and giving us ${params.rating} and your comment as ${params.comments}.` ,
'payload': {
'google': {
'userStorage': userStorage
}
}
});
return console.log("resort location", params.resortLocation);
})
.catch((e => {
console.log('error: ', e);
response.send({
'fulfillmentText' : `something went wrong when writing to database`,
'payload': {
'google': {
'userStorage': userStorage
}
}
});
}))
break;
}
case 'countFeedbacks':{
var docRef = firestore.collection('users').doc(userId);
docRef.get().then(doc => {
if (doc.exists) {
// console.log("Document data:", doc.data());
var dat = doc.data();
response.send({
'fulfillmentText' : `You have given feedback for ${dat.resortLocation} and rating as ${dat.rating}`,
'payload': {
'google': {
'userStorage': userStorage
}
}
});
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
response.send({
'fulfillmentText' : `No feedback found in our database`,
'payload': {
'google': {
'userStorage': userStorage
}
}
});
}
return console.log("userStorage_then_wala", userStorage);
}).catch((e => {
console.log("Error getting document:", error);
response.send({
'fulfillmentText' : `something went wrong while reading from the database`,
'payload': {
'google': {
'userStorage': userStorage
}
}
})
}));
break;
}
You have a couple of options, depending on your exact needs.
Simple: userStorage
Google provides a userStorage object which is persisted across conversations when it can identify a user. This lets you store your own identifier when you need to track when a user returns.
The easiest way to do this is to check the userStorage object for the identifier when your webhook is called. If it doesn't exist, create one using something like a v4 UUID and save it in the userStorage object.
If you are using the actions-on-google library, the code might look something like this:
let userId;
// if a value for userID exists un user storage, it's a returning user so we can
// just read the value and use it. If a value for userId does not exist in user storage,
// it's a new user, so we need to generate a new ID and save it in user storage.
if (userId in conv.user.storage) {
userId = conv.user.storage.userId;
} else {
// Uses the "uuid" package. You can get this with "npm install --save uuid"
var uuid = require('uuid/v4');
userId = uuid();
conv.user.storage.userId = userId
}
If you are using the dialogflow library, you can use the above, but you'll need this line first:
let conv = agent.conv();
If you're using the multivocal library, it does all of the above for you and will provide a UserID in the environment under the path User/Id.
If you're handling the JSON directly, and you are using the Dialogflow v2 protocol, you can get the userStorage object by examining originalDetectIntentRequest.payload.user.userStorage in the JSON request object. You'll set the payload.google.userStorage object in the JSON response. The code is similar to the above and might look something like this:
let userStorage = body.originalDetectIntentRequest.payload.user.userStorage || {};
let userId;
// if a value for userID exists un user storage, it's a returning user so we can
// just read the value and use it. If a value for userId does not exist in user storage,
// it's a new user, so we need to generate a new ID and save it in user storage.
if (userId in userStorage) {
userId = userStorage.userId;
} else {
// Uses the "uuid" package. You can get this with "npm install --save uuid"
var uuid = require('uuid/v4');
userId = uuid();
userStorage.userId = userId
}
// ... Do stuff with the userID
// Make sure you include the userStorage as part of the response
var responseBody = {
payload: {
google: {
userStorage: JSON.stringify(userStorage),
// ...
}
}
};
Note the first line of the code - if userStorage doesn't exist, use an empty object. It won't exist until you send a response that includes storing something in it for the first time, which will happen in the last few lines of this code.
Advanced: Account Linking
You can request users to sign in to your Action using Google Sign In. This can be done just using voice for the simplest of cases and would only interrupt the flow the first time.
After this, your Action is given a JWT which contains their Google ID which you can use as their identifier.
If you're using the actions-on-google library, you can get the ID from the decoded JWT with a line such as:
const userId = conv.user.profile.payload.sub;
In the multivocal library, the ID from the decoded JWT is available in the environment under the path User/Profile/sub
Deprecated: Anonymous User ID
You'll see some answers here on StackOverflow that reference an Anonymous User ID. Google has deprecated this identifier, which was not always a reliable way to verify returning users, and will be removing it 1 Jun 2019.
This code is currently still being sent, but this will be removed starting 1 Jun 2019.

Can't find user by name with Monk / Mongo

I'm working on a CRUD application with Node, Mongo & Monk.
I'd like to find a record by username, and then update it.
But I'm unable to find a record, this code isn't working:
// GET User Profile
router.get('/userprofile', function(request,response){
var db = request.db;
var userName = request.body.username;
var collection = db.get('usercollection');
collection.findOne({
"username": userName
},{},function(e,user){
response.render('userprofile', {
"user": user
});
});
});
The "findOne" method doesn't return anything, and the "user" object ends up empty.
Remove the middle empty object from the signature for the findOne() method signature for the query to work:
Note: The way you are getting the userName is for when the request method is a POST, here you are doing a GET so you need to use the request.query property. More details here
var userName = request.query.username;
collection.findOne({"username": userName}, function(e,user){
response.render('userprofile', { "user": user });
});
If you want to update then you can use the update() method, suppose you want to update the username field to change it to 'foo', the following stub shows how you can do the update:
var u = collection.update({ "username": userName }, { "$set": { username: 'foo' } });
u.complete(function (err, result) {
console.log(err); // should be null
console.log(result); // logs the write result
});
Ok, I found out the problem.
Chridam's code was correct, but I also needed to change my form from a GET to a POST. Once I did that, the form POSTed and mongo could see request.body.username (it was null before) and look up my user using Chridam's code.
After reading Chridam's revised answer, was also able to get it to work with GET.
Now working on the update code..