Security for angularfire - firebase

I am trying to get rid of an error which arises on posting a new entry to an angularfire array. The array holds reports, this array should be able to be written to, but not read from. The rules I have are as follows:
"reports":{
".write":"(auth != null)",
"$report_id":{
//".write":"(auth != null)",// << commented out part
".validate":"!data.exists() && newData.exists() && newData.hasChildren(['reason', 'reportedUser', 'reportingUser', 'time'])",
"reason":{
".validate":"newData.isString() && newData.val().length < 100"
},
"reportedUser":{
".validate":"newData.isString() && newData.val().length < 30"
},
"reportingUser":{
".validate":"auth.uid === newData.val()"
},
"time":{
".validate":"newData.val() <= now"
},
"$other":{
".validate":false
}
}
}
I am calling the write to this array from within a function, the contents of which is as follows:
var reportsArray = $firebaseArray(new Firebase(FBaseURL + "/reports/"));
reportsArray.$add(reportData)
.then(function(ref) {
console.log('user reported');
})
.catch(function(error) {
console.log("Error:", error);
});
Whichever write rule I comment out in rules (also un-commenting the other), the write is successful but the creation of the variable reportsArray presents an error as follows Error: permission_denied: Client doesn't have permission to access the desired data. What's best to do here, ignore the error or am I doing something wrong?

Related

Deleting Documents from Flutter if conditionals are met

I am trying to add a function so that whenever a button is pressed, it will take my variables and look through all documents in my database. Then when it finds a document that matches the variables, it will delete that item. However, I keep getting an error message that I need a ")" after the (doc). Is there a better way to run through all the items to delete them, or am I doing something wrong with the snapshots/forEach statements?
Object deleteUser() {
// Call the user's CollectionReference to add a new user
if(name!="" && type!="" && location!="") {
items.snapshots().forEach(
(doc) => if(doc.data['name']==deleteName && doc.data['type']==deleteType && doc.data['location']==deleteLocation => doc.delete();));
return items;
}else{
return "There was a null error";
}
}
Your forEach loop is what's throwing the error as it isn't valid dart. I cleaned it up, this should work for you.
Object deleteUser() {
// Call the user's CollectionReference to add a new user
if (name != "" && type != "" && location != "") {
for (var doc in items.snapshots()) {
if (doc.data['name'] == deleteName &&
doc.data['type'] == deleteType &&
doc.data['location'] == deleteLocation) {
doc.delete();
}
}
return items;
} else {
return "There was a null error";
}
}

firebase functions stopped working after iam

Im trying to do something absurderlly simple but this whole IAM, Realtime Database, cloud functions misconfiguration are making me waste hours of work in something simple as a hello world.
I have an already populated database (over 300 items):
then i have the following function deployed to firebase cloud:
const actions = [];
const igDatabase = require('firebase-admin').initializeApp({
serviceAccountId: 'actionkeeper#igibo-b0b27.iam.gserviceaccount.com'
}).database("https://igibo-b0b27.firebaseio.com/");
let lastMapRefresh = 0;
let lastUpdateFirebase = 0;
function refreshActions(afterRefresh) {
console.log("refreshing actions");
igDatabase.ref('actions/').orderByChild('timestamp').startAt(lastMapRefresh).once('value').then(function(data) {
if (data != null && data.exists()) {
let bef = actions.length;
actions.length = 0;
actions.push(data.val());
lastMapRefresh = new Date().getTime();
afterRefresh();
}
console.log("actions refreshed before: " + bef + " now: " + actions.length);
}).catch(function(error) {
console.error("Error: " + JSON.stringify(error));
});
}
exports.decrementAction = (req, res) => {
refreshActions(function() {});
}
this function is simple reading a branch on database and populating an array... the purpose of the functio is more complex but im building it and testing slowly... and even this simple method is not working
the firebase rules for that node is:
{
"rules":{
"actions":{
".indexOn":[
"timestamp"
],
".read":"auth != null",
"$act":{
"countdown":{
".write":"auth != null && data.val() - newData.val() == 1 && newData.val() >= 0"
}
}
}
}
}
so ANYBODY logged can read
in the google IAM console i have
so the service account is supposed to have admin powers to database...
but running this function ALWAYS RETURN NULL data
why?
Your code doesn't appear to actually send a respond to the client. Here's your function:
exports.decrementAction = (req, res) => {
refreshActions(function() {});
}
Not once does it ever use res to send a response. It's always going to time out after the default 60s, stuck waiting for you to call res.send() or something similar.
I suggest reviewing the documentation on HTTP triggers to see how to send a response. I imagine your (currently empty) callback function needs to do this, based on what refreshFunctions delivers to it.

Firebase multi-location update validation

I'm having a hard time figuring out how to validate a multi-location update where the updates depend on each other.
Consider the following structure:
"votes": {
"$post_id": {
"$answer_id": {
"$user_id": {
".write": "...",
".validate": "..."
}
}
}
}
"users": {
"$user_id": {
"votes": {
"up": {
".write": "...",
".validate": "..."
},
"down": {
".write": "...",
".validate": "..."
}
}
}
}
The users can vote on posts' answers with -1 / +1 (or remove their votes, so with null as well). So far so good, I can validate that no problem. My problem comes when I want to validate the user's up/down vote counter as well.
Example scenario: user A votes on an answer with +1, which would also increment user B's up counter with 1. How can I validate the up field so that it only gets incremented (or decremented) when there's an actual new vote for that.
Also there are scenarios like when a user has already voted +1 and then changes it directly to -1. I'm having a really hard time validating updates like this.
Should I just consider adding a server layer and do every single updates through the server? Or is my approach totally wrong here (or the data structure?). Adding a server layer would pretty much solve every validation issue, but also would add one more point of failure so I'm trying to avoid that.
Edit:
Update function
function vote(postID: string, answerID: string, author: string, oldVal: number, newVal: number): firebase.Promise<void> {
let voteValue: number = newVal == 0 ? null : newVal; // -1, 0, 1, could be changed to boolean
return this.ref.child(`users/${author}/votes`).once('value', count => {
let updates = {};
updates[`votes/${postID}/${answerID}/${this.authService.current.$key}`] = voteValue;
if (voteValue == 1) {
updates[`users/${author}/votes/up`] = ++count.val().up;
if (oldVal == -1) {
updates[`users/${author}/votes/down`] = --count.val().down;
}
}
if (voteValue == -1) {
updates[`users/${author}/votes/down`] = ++count.val().down;
if (oldVal == 1) {
updates[`users/${author}/votes/up`] = --count.val().up;
}
}
if (voteValue == null && oldVal == -1) {
updates[`users/${author}/votes/down`] = --count.val().down;
}
if (voteValue == null && oldVal == 1) {
updates[`users/${author}/votes/up`] = --count.val().up;
}
this.ref.update(updates);
});
}
When an answer's author's current votes are 0/0 and another user upvotes one of his answers it would create an update like:
"votes/-KM0CMCIQuBsGWQAjhRQ/-KM0CVmhK_7JQcxtdixl/fxpxj1Ky4yVpEeeB5PZviMjqNZv1": 1
"users/spAnCEKTTjX1GgdybQZIlXRI9IG2/votes/up": 1

Unreliable onDisconnect() in Firebase

I wrote following simple presence code in JavaScript (based upon https://firebase.google.com/docs/database/web/offline-capabilities#section-sample):
var app = firebase.initializeApp(config);
var mainRef = app.database().ref();
var session = null;
var connected = false;
function do_sessionSubscribe(subscription) {
if (!subscription.entry) {
subscription.entry = subscription.parent.push(true);
subscription.entry.onDisconnect().remove();
}
}
function do_sessionUnsubscribe(subscription) {
if (subscription.entry) {
subscription.entry.remove();
subscription.entry = null;
}
}
mainRef.child(".info/connected").on("value", function(snap) {
connected = snap.val() === true;
if (session) {
if (connected) {
do_sessionSubscribe(session.subscription);
} else {
// workaround
//do_sessionUnsubscribe(session.subscription);
}
}
});
function closeSession() {
if (session) {
do_sessionUnsubscribe(session.subscription);
session = null;
}
}
function openSession(uid) {
session = { uid: uid, subscription: { parent: mainRef.child("session/user/" + uid), entry: null } };
if (connected) {
do_sessionSubscribe(session.subscription);
}
}
app.auth().onAuthStateChanged(function(user) {
closeSession();
if (user && user.uid) {
openSession(user.uid);
}
});
Security rules:
"session": {
"user": {
"$uid": {
".read": "auth.uid === $uid",
".write": "auth.uid === $uid",
"$session": {
".validate": "newData.val() === true"
}
}
},
}
The idea is that each active connection of a user will create /session/user/$uid/$session upon connecting/signing in and delete it when disconnecting/signing out.
Therefore in order to obtain a list of online users it should be sufficient to get /session/user with shallow=true.
The problem is that sometimes a session isn't cleaned up and stays under /session/user/$uid forever. This is then interpreted like if a user was online all the time.
I discovered that in order to easily reproduce the issue it is sufficient to block access to securetoken.googleapis.com (I use Google authentication), wait an hour and close the browser.
I tried to workaround the problem by calling remove() on disconnection. This cleans up the stale session as soon as the client gets reconnected (this is too late, but better late than never...). However, when user closes it's browser after loosing internet connection and then the auth token expires before sockets time out, the stale session persists forever.
What value of auth.uid is used during checking security rules when auth token used for registering onDisconnect() action is already expired?
How to make this presence system fully reliable without compromising security?

Firebase, multi location updates

Consider the following code for atomic writes across multiple locations in FireBase:
var ref = new Firebase("https://<YOUR-FIREBASE-APP>.firebaseio.com");
var newPostRef = ref.child("posts").push();
var newPostKey = newPostRef.key();
var updatedUserData = {};
updatedUserData["users/"+authData.uid+"/posts/" + newPostKey] = true;
updatedUserData["posts/" + newPostKey] = {
title: "New Post",
content: "Here is my new post!"
};
ref.update(updatedUserData, function(error) {
if (error) {
console.log("Error updating data:", error);
}
});
This approach could be used to update the post at different locations, but how to enforce the atomic update at the server side? (via rules).
How can I make sure that the users cannot update the location /posts/ (via its direct reference) without populating the users/UID/posts/ or vice versa?
There are many possible such "business rules", so I'll pick one and implement that. Let's say that any post that a user refers to must exist. So you can only write to /users/myuid/posts/mypostid if /posts/mypostid exists. I'll also implement basic validation of the posts themselves.
{
"posts": {
"$postid": {
".validate": "hasChildren(['title', 'content'])",
"title": {
".validate": "newData.isString()"
},
"content": {
".validate": "newData.isString()"
},
"$other": {
".validate": false
}
}
},
"users": {
"$uid": {
"posts": {
"$postid": {
".validate": "newData.parent().parent().parent().parent().child('posts').child($postid).exists()
}
}
}
}
}
The biggest trick here is the newData.parent().parent()... bit, which ensures that we get the posts in the new data.
You have a habit of asking things like "how can I ensure that method ABC was used to update the data?", which is rarely the right way to think about matters. In the rules above I focus on validating the structure of the data and really don't care what API calls might lead to that data.

Resources