Firebase role based access fails for new users - firebase

I've followed Secure data access for user groups on the firebase website:
https://firebase.google.com/docs/firestore/solutions/role-based-access
Now my roles are as followed:
service cloud.firestore {
// Roles admin = 0, owner = 1, writer = 2, reader = 3
match /databases/{database}/documents {
function isSignedIn() {
return request.auth != null;
}
function isAdmin() {
return isSignedIn() && get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 0
}
match /projects/{project} {
function getRole(rsc) {
return rsc.data.roles[request.auth.uid];
}
function isOneOfRoles(rsc, array) {
return isSignedIn() && (getRole(rsc) in array);
}
function canRead() {
return isAdmin() || isOneOfRoles(resource, [1, 2, 3])
}
function canWrite() {
return isAdmin() || isOneOfRoles(resource, [1, 2])
}
allow read: if canRead();
allow write: if canWrite();
}
}
}
Now when a new user is created he isn't an admin and he doesn't have projects.
So actually he should be able to create a new project and his list of project should be empty.
I'm doing this query to retrieve all the projects:
getAllProjectsForUser(): Observable<Project[]> {
return from(this.fireStore.collection('projects').snapshotChanges().pipe(map((data => {
const projects: Project[] = [];
data.forEach((doc) => {
projects.push(new Project(doc.payload.doc.id, doc.payload.doc.data()['projectName'], doc.payload.doc.data()['companyName']));
});
return projects;
}))));
}
Now everything works when I'm an admin but it doesn't work for people who don't have any projects and are not admin.
I always get Missing or insufficient permissions.
When I run a query in the rules simulator it gives me:
Error: simulator.rules line [16], column [18]. Null value error. Which is on this line:
return rsc.data.roles[request.auth.uid];
I have no idea how I can solve this?
I tried changing the query like this:
getAllProjectsForUser(uuid: string): Observable<Project[]> {
return from(this.fireStore.collection('projects').ref.where('roles.' + uuid, '<', 4).get()).pipe(map((data => {
const projects: Project[] = [];
data.forEach((doc) => {
projects.push(new Project(doc.id, doc.data()['projectName'], doc.data()['companyName']));
});
return projects;
})));
}
but this ofcourse gives the same problem because there is no project with a roles array.
EDIT
I found out that when I test the rules in the firebase simulator that it works.
It's just the query to retrieve all my projects where I'm owner|writer|reader that doesn't
screenshot data:

Related

how to write Firestore security rules to also allow null value and undefined value in a single field?

I have a field in my Firestore document called lastApproval that should be a timestamp if it has a value.
this is my simplified rules
match /users/{userID} {
allow create: if isValidUserStructure(incomingData())
allow update: if isValidUserStructure(incomingData())
}
function incomingData() {
return request.resource.data;
}
function isValidUserStructure(user) {
return user.email is string
&& user.fullname is string
&& user.lastApproval is timestamp // error in here
}
as you can see, isValidUserStructure function will be used to check when creating and updating user document.
when creating user document, that lastApproval field will be null like this
const data = {
fullname: 'John Doe',
email: 'my#email.com',
lastApproval: null
};
await db.collection('users').doc('userIDHere').set(data);
but when updating document, I only want timestamp.
const data = {
lastApproval: new Date()
};
await db.collection('users').doc('userIDHere').update(data);
and also, I want to pass the security rules if lastApproval is not available, for example, if the user only want to update the fullname like this
const data = {
fullname: "My New Name"
};
await db.collection('users').doc('userIDHere').update(data);
so I want my security rules to accept timestamp, null and undefined value for lastApproval field
I have tried but error
please help :)
There isn't any type null as far as I know. You can find all available types in the documentation.
If you want to check if the value is null then try user.lastApproval == null instead. To check if the lastApproval property exists at first place, try this:
match /collection/{doc} {
allow write: if 'lastApproval' in request.resource.data && (..otherLogic)
}
So you can write your function as:
function isValidUserStructure(user) {
return user.email is string
&& user.fullname is string
&& (('lastApproval' in user && user.lastApproval is timestamp) || !('lastApproval' in user))
}

Firestore Rules to update 2 arrays with Batch Write

I am doing a batch write like so:
const batch = this.afs.firestore.batch();
const studentID = this.afs.createId();
const classID = this.afs.createId();
const studentRef = this.afs.doc(`students/${studentID}`).ref;
batch.set(studentRef, {
name: 'tom',
classes: firebase.firestore.FieldValue.arrayUnion(classID)
});
const classRef = this.afs.doc(`classes/${classID}`).ref;
batch.set(classRef, {
name: 'calculus',
students: firebase.firestore.FieldValue.arrayUnion(studentID)
});
await batch.commit();
And I want to ensure that either:
students/studentID/classes array can only be changed if classes/classID/students array also changed with the correct IDs
this is not enforced if that field is not updated
one field cannot be deleted unless the other field is deleted
So, I am thinking this:
match /students/{studentID} {
allow read;
allow write: if noChange('classes') ||
matchWrite(studentID, 'classes', 'students');
}
match /classes/{classID} {
allow read;
allow write: if noChange('students') ||
matchWrite(classID, 'students', 'classes');
}
function noChange(field) {
return !(field in request.writeFields);
}
function getVal(field) {
return resource.data[field].removeAll(request.resource.data[field])[0];
}
function matchWrite(VAL1, VAL2, VAL3) {
return VAL1 in
getAfter(/databases/$(database)/documents/$(VAL2)/$(getVal(VAL2))).data[VAL3];
}
delete - delete for references...
// allow delete: if noChange('classes') || matchDelete('classes', 'students');
// allow delete: if noChange('students') || matchDelete('students', 'classes');
// function matchDelete(VAL1, VAL2) {
// students/STUDENTID/classes array CLASSID (being removed)
// must eq classes/CLASSID/students array STUDENTID (being removed)
// and the other way around
// }
I am getting boggled down on the last part. Since the batches are atomic, I would think I can use getAfter() somehow.
How would I ensure atomic rules or nothing?
J

Error on uploading file to firebase cloud storage: "Listing objects in a bucket is disallowed for rules_version = 1"

I've setup firebase auth and cloud storage on my application. Whenever I try to upload a file, however, I'm getting an error I can't find any information about. The error is a 400: Bad Request with the following message
Listing objects in a bucket is disallowed for rules_version = "1". Please update storage security rules to rules_verison = "2" to use list.
I can't seem to find anything about updating security rules_version. Note, when I look at the firebase console, the upload actually successfully goes through, but the HTTP return is still the error above. What does it mean by listing objects, and how can I update my security rules?
For more information, my upload code (in Kotlin) is
fun uploadImage(uri: Uri, path: String): Task<Uri> {
val storageRef = FirebaseStorage.getInstance().reference
val storagePath = storageRef.child(path)
return storagePath.putFile(uri).continueWithTask{ storageRef.downloadUrl }
}
I call it with
public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode in 0..2 && resultCode == Activity.RESULT_OK) {
val cropImageUri = CropImage.getActivityResult(data).uri
val systemTime = System.currentTimeMillis()
val path = "$userId/$systemTime"
//IMAGE UPLOAD HERE:
FirebaseImageResource.uploadImage(cropImageUri, path)
.addOnCompleteListener {
if (it.isSuccessful) {
GlideApp.with(this)
.load(cropImageUri)
.into(imageViewForPosition(requestCode)!!)
imageUris[requestCode] = it.result.toString()
}
}
}
}
My firebase rules are the default:
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write: if request.auth != null;
}
}
}
I'm also successfully authing with Facebook Login
override fun onSuccess(loginResult: LoginResult) {
val credential = FacebookAuthProvider.getCredential(loginResult.accessToken.token)
auth.signInWithCredential(credential)
}
(It lacks a success listener right now, but I know it's working because when I don't use it, I get an unauthorized error instead, and the file doesn't actually upload)
You need to prepend this to your Firebase Storage rules:
rules_version = '2';
An example is this:
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write: if request.auth != null;
}
}
}
I suspect that this is a case of a not-so-helpful error message.
Just replace your upload code from
fun uploadImage(uri: Uri, path: String): Task<Uri> {
val storageRef = FirebaseStorage.getInstance().reference
val storagePath = storageRef.child(path)
return storagePath.putFile(uri).continueWithTask{ storageRef.downloadUrl }}
to
fun uploadImage(uri: Uri, path: String): Task<Uri> {
val storageRef = FirebaseStorage.getInstance().reference
val storagePath = storageRef.child(path)
return storagePath.putFile(uri).continueWithTask{ storagePath.downloadUrl }}
I solved it with the following setps
Sign in Firebase console
Select project
Click Storage button
Select Rules tab
Insert top "rules_version = '2';"

Allow only one document to be created in Firestore, disallow update, edit etc

I'm writing Quiz app of sorts using VueJS and Firebase firestore.
So far I've made everything except this one last part.
Users are allowed to answer questions without being logged in.
And at the final stage, there is one last question. Everyone can answer this question, but I need to be able to detect who is first.
So far I've tried with checking if answers collection is empty, this works, but response time is the issue and I can reproduce easily two or more users answering at the same time and having message they are the winners.
I'm currently trying with transactions, but cannot figure it out how to catch if document already exists. Here's the sample code:
let vm = this;
fsdb.runTransaction(function (transaction) {
let voteDocRef = fsdb.collection('final_vote').doc('vote');
return transaction.get(voteDocRef).then(function (voteDoc) {
if (!voteDoc.exists) {
voteDocRef.set({voted: true}).then(function () {
vm.trueAnswer(index);
return 'set executed!';
}).catch(function () {
vm.falseAnswer(index);
throw 'Someone already voted!';
});
return 'Document created!';
} else {
throw 'Someone already voted!';
}
});
vm.trueAnswer and vm.falseAnswer are the methods I'm using to show the popup message.
And this is the method that's triggered once the user submits the answer.
At first I've tried with rules that everyone can read, write ... but now I'm trying to limit write only if document doesn't exist. Here's the current rule set:
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if false;
}
match /{collectionName}/{docId} {
allow create: if collectionName == 'final_vote' && docId == 'vote';
}
}
}
So far this doesn't work as expected.
Any ideas on how to approach this?
Would you try the following code?
let vm = this;
fsdb.runTransaction(function (transaction) {
let voteDocRef = fsdb.collection('final_vote').doc('vote');
return transaction.get(voteDocRef).then(function (voteDoc) {
if (voteDoc.data().voted !== true) {
voteDocRef.set({voted: true}).then(function () {
vm.trueAnswer(index);
return 'set executed!';
}).catch(function () {
vm.falseAnswer(index);
throw 'Someone already voted!';
});
return 'Document created!';
} else {
throw 'Someone already voted!';
}
});
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if false;
}
match /final_vote/{docId} {
allow create, update: if true;
}
}
}
I think that final_votes is better than final_vote as collection name.

Firestore transactions with security rules making reads

I want to create two documents
Account/{uid} {
consumerId: ... //client generated id
}
Consumer/{consumerId} {
...
}
and I have a security rule for the consumer collection
match /Consumer/{consumerId} {
allow create: if (consumerId == get(/databases/$(database)/documents/Account/$(request.auth.uid)).data['consumerId'];
}
I need to ensure that an account can only add a consumer document with a consumerId corresponding to the one in their Account document. Both documents should be created together. I've been trying to do this with transactions but I keep getting the error "Transaction failed all retries.". Whats going wrong and how do I fix it?
The data variable is an object and not an array, so you should use data.consumerId instead of data['consumerId']:
match /Consumer/{consumerId} {
allow create: if consumerId == get(/databases/$(database)/documents/Account/$(request.auth.uid)).data.consumerId;
}
I ended up accomplishing this with a batch write and security rules.
match /consumer/{cid} {
function isNewResource() { return resource == null; }
allow create: if isRegistered();
allow read, update: if isNewResource();
}
And then client side with something along the lines of
createThing() {
const db = firebase.firestore();
const { uid, displayName } = this.auth.currentUser;
const batch = this.db.batch();
// Essentially generating a uuid
const newConsumerRef = db.collection("consumer").doc();
// Update the user doc
batch.update(
db.collection('/users').doc(uid),
{ consumerID: newConsuemrRef.id }
);
// Update the admins field in the new doc
batch.set(newConsumerRef, {
admins: {
[uid]: displayName,
},
});
return batch.commit();
}
My problem was the same, but the write to the field in the collections actually needed to be to an object key, so it looked a little funkier
batch.update(
db.collection('/users').doc(uid),
{ [`adminOf.${newRef.id}`]: 'some special name' }
);

Resources