I have a question about the firebase realtime database rules.
Somebody creates an account and that account creates a path in the realtime database:
The structure is simple (key, userid, other data).
This are my rules:
{
/* Visit https://firebase.google.com/docs/database/security to learn more about security rules. */
"rules": {
"waitingForApproval": {
"$uid": {
".write": true,
".read": "auth != null && $uid === auth.uid"
}
},
}
}
But now comes the question. How can I allow to let users write to this object? Everyone who has the code (see BQUyhq)w3D) can write to the object id. They can't write to it when they don't have the code.
Is something possible like that? If so, how can I do that.
I can think of two ways to approach something like this:
1. Using a cloud function:
Set the rule for writing to false and use a cloud function to make updates. The cloud function would take the code as an input, verify it matches the expected code, and perform the updates on the user's behalf. E.g.:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
exports.updateObject = functions.https.onCall((data, context) => {
const uid = data.uid;
const code = data.code;
const codeRef = admin.database().ref(`waitingForApproval/${uid}/code`);
return codeRef.once('value').then((codeSnapshot) => {
if (codeSnapshot.val() === code) {
// Make updates on the user's behalf
}
});
});
2. Storing user entered code in the DB:
Add a user editable section where users can set their codes and validate against that, e.g.:
The DB after a user has entered a code:
"userCodes": {
"bXUQ6PRNqvOgjwoAlm6HmYuWiYo1": {
"BQUyhq)w3D": true,
},
...
}
Set your rule to check if the user has set the code for the object:
"waitingForApproval": {
"$uid": {
".write": "root.child('userCodes').child($uid).child(data.child('code').val()).val() == true"
".read": "auth != null && $uid === auth.uid"
}
},
Which essentially checks if userCodes/{uid}/{code} is set to true before allowing the write.
(Note that using this method your codes cannot contain the characters that firebase doesn't support in its keys: . $ # [ ] /)
Related
I have a collection of documents. Each document has child uid which is a reference to the owner. Exemplary document:
{
"uid": "slfjs092320i3jf023jf",
"content": "Example"
}
I want to store them under /documents collection and allow only users with matching uid to store and retrieve them. I created the following rules:
{
"rules": {
"documents": {
"$documentId": {
".read": "data.child('uid').val() === auth.uid",
".write": "newData.exists() && newData.child('uid').val() === auth.uid || data.exists() && data.child('uid').val() === auth.uid",
".indexOn": ["uid"]
}
}
}
}
Unfortunatelly when I want to retrieve user's documents I receive permission denied. To retrieve documents I use:
export default getUserDocuments = () => {
const userUid = firebase.auth().currentUser.uid;
return firebase.database()
.ref('/documents')
.orderByChild('uid')
.equalTo(userUid)
.once('value');
};
How do I query user's documents? I guess the permission denied is related to read restriction required to perform the query.
Firebase Database enforced read permission when you attach a listener. To be able to read from /documents, you will need to have read permission on /documents. Since that isn't the case with your security rules, the listener is rejected.
This may be counter-intuitive initially, and means that security rules cannot be used to filter data in the way you are trying. This is known as rules are not filters in the documentation, and has been the topic of many previous questions. I recommend you check out some of those, and report back if you have more questions.
Have a look on query based rules.
Rules are not filters, as Frank said, and he is right, but you can make some queries to works and achieve something similar to what you were looking for with this kind of stuff :
baskets": {
".read": "auth.uid != null &&
query.orderByChild == 'owner' &&
query.equalTo == auth.uid" // restrict basket access to owner of basket
}
And then, this will work :
db.ref("baskets").orderByChild("owner")
.equalTo(auth.currentUser.uid)
.on("value", cb)
And this, won't :
db.ref("baskets").on("value", cb)
I want to ensure that a new user can be created (from the client), but only an authenticated user can read or write an existing object.
I have a simple rule set:
{
"rules": {
"users": {
"$uid": {
".read": "auth != null && auth.uid === $uid",
".write": "!data.exists() || auth.uid === $uid"
}
}
}
}
I am calling createUser and then in the callback I'm trying to add an entry to my own users object:
const usersRef = ref.child('users');
const userEntry = {
[userData.uid]: {
created: new Date().getTime()
}
};
usersRef.set(userEntry)
I would have thought that even though the user is not yet logged in, they should have write permission because of !data.exists(). Yet I am getting a PERMISSION_DENIED error.
If I set ".write": true on the users level then it will cascade (and override?) my inner rules won't it?
Edit:
This fails even with:
"users": {
"$uid": {
".read": true,
".write": true
}
}
Thanks.
I think I initially misunderstood the problem. In your current callback, you are trying to overwrite the entire users level because of how set works.
You would really want to set only the thing that doesn't exist:
const userRef = ref.child('users').child(userData.uid);
const userEntry = {
created: new Date().getTime()
};
userRef.set(userEntry);
Then, I think that your existing rules would work.
I think this a confusing question because creating a user and writing to the database are completely different things. So i will just show how i do it in my app.
First step is creating the user
Next log the user in because creating doesn't automaticly log the user in (I do this in the callback function of create user)
Last step is writing the user data to firebase
I use the following rule to make sure each user can only write to his own node in firebase (documentation):
{
"rules": {
"users": {
"$user_id": {
".write": "$user_id === auth.uid"
}
}
}
}
And one thing to keep in mind is that set() will replace any existing data at that path. So make sure you use the uid of the user and not the users node.
Finally i want to point out a huge flaw in the rules you posted in your question:
".write": "!data.exists() || auth.uid === $uid"
This rule states you can write if there isn't any data yet OR you have the correct uid. The first part of this statement is the problem because ANYONE can write to this locaion when there isn't any data. Also because $uid is a dynamic path you can add anything there like:
"users": {
"WoWIjustMadeThisUp": {
"nice": "Some huge value making you go over your limit"
}
}
If you want users to only write an initial value and after that won't be able to edit it just use a validate rule to check if there is already data at that location.
After a user registers, we create a username for the user under /users child.
How can I setup my rules so that only the server can create new users and user data cannot be modified by other users.
Javascript
<script>
var ref = new Firebase("https://myapp.firebaseio.com");
var usersRef = ref.child('users');
var usernameField = $('#usernameInput');
var emailField = $('#emailInput');
var passField = $('#passInput');
$('#submitBtn').click(function() {
ref.createUser({
email : emailField.val(),
password : passField.val()
}, function(error, userData) {
if (error) {
console.log("Error creating user:", error);
} else {
console.log("Successfully created user account with uid:", userData.uid);
usersRef.push({
uid: userData.uid,
username: usernameField.val()
});
}
});
});
</script>
Current rules:
{
"rules": {
"users": {
".indexOn": "username",
"$uid": {
".write": true,
// grants read access to any user who is logged in with an email and password
".read": "auth !== null && auth.provider === 'password'"
}
}
}
}
To set up something so that "only the server can create new users", you can access your database using their Server SDK. This way, you can set rules that only apply to access to the database from the server.
However, if you want a web-based UI for admins to create new users, you need to write a Node.js or Java backend web server (utilizing the Server SDK). You can't use the client-side JS SDK, or Firebase hosting.
Just follow the instructions in the Server Side setup section of their documentation.
Then add the databaseAuthVariableOverride option to initializeApp:
var firebase = require("firebase");
firebase.initializeApp({
serviceAccount: "path/to/serviceAccountCredentials.json",
databaseURL: "https://databaseName.firebaseio.com",
databaseAuthVariableOverride: {
// maybe use something more descriptive and unique
uid: "server-side-app"
}
});
Then the following as your rules (warning: not tested):
{
"rules": {
"users": {
".indexOn": "username",
// grants write access if the authenticated user is accessing
// via the Server SDK
".write": "auth != null && auth.uid === 'server-side-app'",
// grants read access to any user who is logged in with an email/password
".read": "auth != null && auth.provider === 'password'"
}
}
}
There is no concept of "only the server" when you use Firebase. Each user writes their own data, so to secure that you need to use Firebase's User Based Security.
In this case the rules are fairly simple: you want each user to be able to only write to their own node. You can do this by modifying your rules to:
{
"rules": {
"users": {
".indexOn": "username",
"$uid": {
// grants write access to the owner of this user account
// whose uid must exactly match the key ($user_id)
".write": "auth.uid === $uid",
// grants read access to any user who is logged in with an email and password
".read": "auth.uid !== null && auth.provider === 'password'"
}
}
}
}
Note that I copied the .write rule from the first example in the Firebase documentation on User Based Security. So it's probably a good idea to read that page (again), since it explains a lot more than just the answer to this question.
I am trying to setup private chat abilities in an app that I am working on and I'm having a bit of trouble wrapping my head around denormalizing the data/setting up the rules properly.
After doing some reading, I realize that rules are all or nothing so using rules to filter is not an option.
I've sketched out my basic idea on paper, and have pasted it below. Basically there would be two main routes, users and chats.
Users would just be a keyed list, which each key matching an authenticated user. Then inside each member of the list I would just have each chat that the said user is in listed as a key.
For the chats route I'd have a list of all of the chats.
Now for the rules.
Users would only be able to read their data in the list where the key matched their uid. For the write i'm less confident. I'm thinking I have to let anyone with authentication write, otherwise the user starting the chat could not notify others of the new chat by playing the chat id in their chat list in the users route.
For the chats rules both read and write would only be allowed if the user is authenticated and the chat key is located inside their data in the user route.
Does the seem like I'm going in the right direction?
users:{
user1:{
chat1: true,
chat2: true
...
},
user2:{
chat1: true,
chat3: true
....
}
}
chats:{
chat1:{
lastUpdate: timestamp,
messages:{
0:{
from: user1
to: user2,
message: some message
}
...
}
}
}
rules:{
.read: false,
.write: false,
users:{
$user_id:{
.read: auth != null && $user_id == auth.uid,
.write: auth != null //not sure here as other users need to write here if the start a new chat
}
},
chats:{
$chat_id: {
.read: auth != null && root.child('users').child($chat_id).contains(auth.id),
.write: auth != null && root.child('users').child($chat_id).contains(auth.id)
}
}
}
I've been playing with this more, so here is one option (by no means am I suggesting this is the best way to do it)
Rules:
{
"rules":{
".read": false,
".write": false,
"users":{
"$user_id":{
".read": "auth != null && $user_id == auth.uid",
".write": "auth != null" //not sure here as other users need to write here if the start a new chat
}
},
"chats":{
"$chat_id": {
".read": "auth != null && root.child('users').child(auth.uid).child('chats').hasChild($chat_id)",
".write": "auth != null && (root.child('users').child(auth.uid).child('chats').hasChild($chat_id) || !data.exists())"
}
}
}
}
then for users I have a structure like this:
users:{
someUserId:{
chats:{ //embedded a second level so I can save firebaseObj.someUserId to get the keys more easily
someChatId: true //and repeat for each chat
}
}
}
Chats are like this:
chats:{
someChatId:{
//chat data
}
//more chat objects
}
I wouldn't be surprised if there is a much better way to do this, but at least this may be a start for those who are stuck. I'll try to remember to update this if/when I get a better solution.
I'm struggling to set the proper security rules for my application.
An overview of the application I'm writing is that users can register themselves using email and password (I'm using Firebase Simple Login for this which works perfectly). Once logged in, user can add their todos.
angularFire('https://<firebase>/firebaseio.com/todos', $scope, 'todos');
And to add a new todo against any user, I simply update the todos model.
$scope.todos.push({
user: 'a#b.com',
todo: 'What to do?'
});
This security rules I'm using to restrict non-registered user to add any todo:
{
"rules": {
".read": true,
"todos": {
".write": "auth != null",
".validate": "auth.email == newData.child('user').val()"
}
}
}
But it does not allow even an authenticated user to write any data and throwing an error,
"FIREBASE WARNING: on() or once() for /todos failed: Error: permission_denied."
But If I add the following data in simulator then it works as expected.
{user: "a#b.com", todo: 'What to do?'}
Here is the log:
/todos:.write: "auth != null"
=> true
/todos:.validate: "auth.email == newData.child('user').val()"
=> true
/todos:.validate: "auth.email == newData.child('user').val()"
=> true
Write was allowed.
push adds a new child with a randomly generated ID (in chronological order) to /todos. So, newData isn't pointing to what you think it is pointing to. Change your rules to:
{
"rules": {
".read": true,
"todos": {
"$todoid": {
".write": "auth != null",
".validate": "auth.email == newData.child('user').val()"
}
}
}
}
Update: Above rule is valid but angularFire currently writes the whole array back to the server causing the auth to fail. You can use angularFireCollection instead, to only write the new TODO back, like so:
$scope.todos = angularFireCollection(new Firebase(URL));
$scope.todos.add({user: 'a#b.com', todo: 'What to do?'});
There's an open issue to optimize angularFire's behavior when new items are added to the list, but in the meantime you can use angularFireCollection to get the right behavior.