Firestore security rules accessing arrayUnion data - firebase

I am trying to develop a chat app, for now it's purely a playground for future use.
For that app, I want to send an arrayUnion with an object inside it, that contains the message details.
On my DB, I have a document with an array of messages.
I want to impose security rules on said object, so that it'll check that it has only the items and types that I define.
Without any rules, it works as intended, but I just can't access the items in the received object in order to check them.
Here's what I'm sending on my frontend:
const message = {
timeStamp: Timestamp.now(),
text: currentText,
sender: userData.uid,
};
await updateDoc(doc(db, 'chats', chatId), {
messages: arrayUnion(message),
});
And this is the rules function that I'm trying to impose:
function checkChatMessage(message, requestUid){
return message.keys().hasAll(['timeStamp', 'text', 'sender']) &&
message.timeStamp is timestamp &&
message.text.size()>0 &&
message.text.size()<256 &&
message.text is string &&
message.sender is string &&
message.sender == reqUid;
}
I have tried passing that function request.arrayUnion[0], request.resource.data.message, request.resource.data.map and some more variations, but none seems to work.

Related

how to allow an access even if the document field is not available in firestore security rules?

so I add a new field in my user document called numberOfFollowers, so the brand new user will have this field but, old user doesn't has this numberOfFollowers field.
in security rules I want to set that the numberOfFollowers should be a number. and if old user does not have this field then it still allowed to update their document.
so I set the security rules like this
match /users/{userID} {
allow update: if isValidUserStructure(request.resource.data)
}
function isValidUserStructure(user) {
return (user.numberOfFollowers is number || user.numberOfFollowers == null)
&& user.banned is bool
&& user.contactNumber is string
&& user.createdAt is timestamp
&& user.domicile is string
&& user.email is string
}
but the access is always denied for old user that doesn't has this field because of this line (user.numberOfFollowers is number || user.numberOfFollowers == null)
To check if a field is NOT present on some document, you should check the data object, which is a Map type object, using the documented in operation that checks for presence of a key:
function isFieldPresent(data, fieldName) {
return fieldName in data;
}
You can then choose what to do in this case.
Since null is a valid field value in Firestore, checking for null as you're doing now is definitely not the same thing as checking for non-existence.

Cloud Firestore Custom Claims when null

I am blocking users by adding to their token using cloud functions
exports.blockUser = functions.https.onCall(async(data, context) => {
const user = await admin.auth().getUserByEmail(data['email']);
if(context.auth.token.admin){
admin.auth().setCustomUserClaims(user.uid, {
block: true
});
console.log(data['email'] + " has been blocked");
return 1;
}else{
return 2;
}});
In my rules i have set to allow read if block == null, because users that are not blocked will not have that data on their token.
allow read: if request.auth.token.block == null;
However this does not work and permission is denied.
I have tried the other way round just to ensure that the token data exist
allow read: if request.auth.token.block == true;
This allowed only block users to be able to read data. and it work. which means there is no issue with the data on the token.
What can i do to allow users which dont have the "block" property on their token to be able to read data?
With security rules, a missing property is not the same as the property equating to null. You should instead check to see if the block property actually exists, and also check if its value should restrict access
allow read: if !("block" in request.auth.token) || equest.auth.token.block == false;
Reference the documentation for Map (request.auth.token is a Map).

Firestore Cloud Function - Get request data object in onUpdate/onCreate

When writing firebase rules, you can access the request data via request.resource.data. This is useful because you can look at the nature of the request to determine its intent, its write target and permit or deny. This enables merging properties into an object within a document owned by a user, vs using a nested collection of documents.
I would like to access the same request data in the cloud function callbacks update/write/etc, but I don't see it, and I'm left to do an object compare with change.before and change.after. It's not a problem, but did I miss something in the documentation?
Per documentation: https://firebase.google.com/docs/firestore/extend-with-functions
exports.myFunctionName = functions.firestore.document('users/marie').onWrite((change, context) => {
// ... the change or context objects do not contain the request data
});
I had the exact same question when I realized that a function listening for updates was being triggered regardless of the property being updated, despite having a 'status' in data check. The catch that data represented handler.after.data. Although I wasn't able to access the request data, either from the handler or from the context, I was able to solve the problem by adding an additional check which serves the same purpose. Namely:
const dataBefore = handler.before.data();
const dataAfter = handler.after.data();
if (status in dataBefore && status in dataAfter) {
if (dataBefore.status === 'unpublished' && dataAfter.status === 'published') {
// handle update
}
}

userId gets into the document magically

This Meteor code is working fine, but I would like to ask if it is the way Meteor does things or it is a un predictable side effect that may change under some condition later.
The things is that when I do
DisplayCol.insert({action: 'task1', element: 'p', value: value_variable});
Meteor also inserts the correct userId (using 2 different browsers logged in as 2 different users) which I did not explicitly included in the document.
The above line of code is inside a server side function which is called from Meteor method.
here is the relevant information;
//lib/collection.js
DisplayCol = new Mongo.Collection('displayCol');
//server.js
Meteor.publish('displayCol', function () {
return DisplayCol.find({userId: this.userId});
});
DisplayCol.before.insert(function (userId, doc) {
doc.userId = userId;
});
In the docs of Collection hooks > Additional notes > second bulleted paragraph says:
userId is available to find and findOne queries that were invoked within a publish function.
But this is a collection.insert. So should I explicitly include the userId in the document or let the collection hook do its hidden magic? Thanks
No, there is no hidden magic in that code, your before hook is inserting the userId field in the document.
When you do an insert like this,
DisplayCol.insert({action: 'task1', element: 'p', value: value_variable});
the doc that your are inserting is { action: 'task1', element: 'p', value: value_variable }
Because, you have this hook,
DisplayCol.before.insert(function (userId, doc) {
doc.userId = userId;
});
it changes the doc before inserting into collection. So the above hook will change your doc to {action: 'task1', element: 'p', value: value_variable, userId: 'actual-user-id' }
This is the expected behaviour.
Regarding your other point in the question,
userId is available to find and findOne queries that were invoked
within a publish function.
Previously userId parameter in the find and findOne returns null, so user needs to pass userId as a parameter as mentioned in this comment. Additional notes mentions that the hack is not required any more. It has nothing to do with inserting userId field into the collection document.
To have a quick test, remove the DisplayCol.before.insert hook above, you will not see userId field in the newly inserted documents.
UPDATE
Just to clarify your doubt further, from the 4th point in the docs that you provided
It is quite normal for userId to sometimes be unavailable to hook
callbacks in some circumstances. For example, if an update is fired
from the server with no user context, the server certainly won't be
able to provide any particular userId.
which means that if the document is inserted or updated on the server, there will be no user associated with the server, in that case, userId will return null.
Also you can check the source code yourself here. Check the CollectionHooks.getUserId method, it uses Meteor.userId() to get the userId.
CollectionHooks.getUserId = function getUserId() {
var userId;
if (Meteor.isClient) {
Tracker.nonreactive(function () {
userId = Meteor.userId && Meteor.userId(); // <------- It uses Meteor.userId() to get the current user's id
});
}
if (Meteor.isServer) {
try {
// Will throw an error unless within method call.
// Attempt to recover gracefully by catching:
userId = Meteor.userId && Meteor.userId(); // <------- It uses Meteor.userId() to get the current user's id
} catch (e) {}
if (!userId) {
// Get the userId if we are in a publish function.
userId = publishUserId.get();
}
}
return userId;
};

angularFireCollection is not returning any data

I have no issues when using implicit updates (angelFire). However I need for some of my data use explicit updating. So I implemented angelFireCollection on the exact same ref I was using previously but despite the console.log explicitly saying that the read was granted and trying it with both with the onloadcallback and without, I don't get data directly into my assigned variable AND once the callback fires I get a strange looking object that DOES contain the data but not in the form I expect. My scope variable ends up with an empty collection. Never gets populated. Here is the code:
var streamController = function ($rootScope, $scope, $log, $location, angularFireCollection, profileService) {
//Wait for firebaseLogin...
$rootScope.$watch('firebaseAuth', init);
function init() {
if ($rootScope.firebaseAuth == false) {
return
};
var refUsers = new Firebase($rootScope.FBURL+'/users/'+$rootScope.uid);
$scope.profile = angularFireCollection(refUsers, function onload(snapshot) {
console.log(snapshot)
});
};
};
myApp.gwWebApp.controller('StreamController', ['$rootScope', '$scope', '$log', '$location', 'angularFireCollection', 'profileService',
streamController]);
}());
Here is what the console.log looks like ( ie; what snapshot looks like ):
>snapshot
T {z: R, bc: J, V: function, val: function, xd: function…}
Here is the earlier message before the snapshot was returned:
Firebase Login Succeeded! fbLoginController.js:16
FIREBASE: Attempt to read /users/529ccc5d1946a93656320b0a with auth={"username":"xxxxxxx#me.com","id":"529ccc5d1946a93656320b0a"} firebase.js:76
FIREBASE: /: "auth.username == 'admin'" firebase.js:76
FIREBASE: => false firebase.js:76
FIREBASE: /users firebase.js:76
FIREBASE: /users/529ccc5d1946a93656320b0a: "auth.id == $user" firebase.js:76
FIREBASE: => true firebase.js:76
FIREBASE:
FIREBASE: Read was allowed.
and finally the desired binding that ends up with an empty array: again from the console:
$scope.profile
[]
Anyone know what I could possibly be doing wrong?? This is like 5 lines of code. Frustrating.
I have put stops in angelFireCollection factory function and can see that the data is getting added to the collection in the callbacks inside that function but my binded variable never gets updated.
UPDATE
Ok experimenting with a plnkr. It seems that angularFireCollection EXPECTS your returning a LIST of items. The snapshot returns properly if you inspect snapshot.val() it will be whatever object structure was stored in firebase. IF you use angularFireCollection it does indeed bind to the variable HOWEVER it turns a non-list object into a garbled mess and you can not access the object user the normal dot operator. This is either a bug or it is a severe limitation of angularFireCollection which will cause me to revaluate how easily I can use firebase as the backend. I can't share my plnkr because it is accessing non-public data but tomorrow if i have time I will create a public firebase with an object store and demonstrate.
Ok. So it appears that indeed angularFireCollection is MEANT to be array based. Which is fine. It would be VERY helpful if the angularFire documentation was updated to make that clear. As such it is not an implicit vs explicit update technique.
For an explicit non-array based approach I have come up with the following code. Had I not been mislead by the documentation I would have gone down this path originally.
var MainCtrl = function($scope, angularFire) {
$scope.test = {};
var _url = 'https://golfwire.firebaseio.com/tmp';
var _ref = new Firebase(_url);
var promise = angularFire(_ref, $scope, 'implicit');
promise.then ( function(data){
$scope.explicit=angular.copy($scope.implicit );
});
}
You then work locally with the 'explicit' copy and when ready just update the 'implicit' by assigning: $scope.implicit = $scope.explicit.
Here is a plnkr: http://plnkr.co/edit/bLJrL1

Resources