Allow access to customer data if user is part of privileged collection - google-cloud-firestore

The database structure is like so:
suppliers(collection)>user_email(document)
customers(collection)>user_email(document)>foo(field)
And the current firestore.rules is like so:
match /databases/{database}/documents {
//Base rule - fully restrictive
match /{document=**} {
allow read, write: if false;
}
//Checks if user is registered in the suppliers section
function isSupplierTeam(request) {
return exists(/suppliers/$(request.auth.token.email));
}
// Supplier self-data access
match /suppliers/{email} {
allow read, write: if request.auth.token.email == email;
}
match /suppliers/{email}/{document=**} {
allow read, write: if request.auth.token.email == email;
}
// Checking the Customer Sub-Section
match /customers/{email} {
allow read, write: if request.auth.token.email == email || isSupplierTeam(request);
}
match /customers/{email}/{document=**} {
allow read, write: if request.auth.token.email == email || isSupplierTeam(request);
}
}
And the query run in javascript, when logged in as a "supplier" is:
db.collection("customers").get()
.then((querySnapshot) => {
querySnapshot.forEach((doc) => {
console.log(doc.id);
console.log(doc.data());
});
})
.catch((error) => {
console.log("Error getting documents: ", error);
});
It doesn't seem to work and instead triggers a missing/insufficient permission error. How should I tweak my firestore.rules to allow suppliers to access the customer data?

So this works:
//Checks if user is registered in the suppliers section
function isSupplierTeam(request) {
return exists(/databases/$(database)/documents/suppliers/$(request.auth.token.email));
}
It seems like the full "URL" is needed to access the relevant firestore document.

Related

mongodb/mongoose: Save unique value if data is not null in from nestjs

I am trying to save data in MongoDB. I want to store unique data when data is not null. However, I want to allow multiple null values in the unique identifier.
My sample schema:
#Schema()
export class Contact extends Document {
#Prop({ unique: true, sparse: true, require: true })
email: string;
#Prop({ default: '+1' })
countryCode: string;
#Prop({ unique: true, sparse: true })
mobile: string;
}
In this case, a mobile number is not required. User can add their contact information with or without providing a mobile number. If the user sends their mobile number that should be unique. So, I need to allow multiple null values in the mobile field. However, that field should be unique when the user provides any mobile number.
Empty entries seem to get the value null so every entry without mobile crashes with the unique identifier.
Is there any way to solve this problem either from the database layer or the application layer?
I am using NestJS for developing my API.
A unique index still does not allow multiple docs with a field of null. You need to transform your data payload by dropping the null field before you save your docs in MongoDB. A transform pipe will help you to handle this issue. Here is a transform pipe that you can use for this purpose:
#Injectable()
export class NullValidationPipe implements PipeTransform {
private isObj(obj: any): boolean {
return typeof obj === 'object' && obj !== null;
}
private dropNull(values) {
Object.keys(values).forEach((key) => {
if (!(key === 'password' || key === '_id')) {
if (this.isObj(values[key])) {
values[key] = this.dropNull(values[key]);
} else if (Array.isArray(values[key]) && values[key].length > 0) {
values[key] = values[key].map((value) => {
if (this.isObj(value)) {
value = this.dropNull(value);
}
return value;
});
} else {
if (values[key] === null || values[key] === undefined) {
delete values[key];
}
}
}
});
return values;
}
transform(values: any, metadata: ArgumentMetadata) {
const { type } = metadata;
if (type === 'param' || type === 'custom') return values;
else if (this.isObj(values) && type === 'body') {
return this.dropNull(values);
}
throw new BadRequestException('Validation failed');
}
}
Use this pipe in the controller and this pipe will drop all incoming null fields which will come with the request payload.
You can also check nest pipe transform docs: https://docs.nestjs.com/techniques/validation

Firestore Rules: Only show documents if document field is false

I have a NSFW system where it checks the document if it is NSFW and if it is it updates the document field isNSFW to true. That works just fine but now I wanted to not show those documents to all users via settings a rule instead of querying it out.
This is what I have but it's not working...
javascript
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.auth.uid != null;
}
}
match /users/{user} {
allow read: if true;
}
match /docs/{doc} { // THIS HERE
allow read: if resource.data.isNSFW == false;
}
}
I tried adding request. before the resource and it still didn't work.
UPDATE:
javascript
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// match /{document=**} {
// allow read, write: if request.auth.uid != null;
// }
match /users/{user} {
allow read: if currentUser().uid != null;
}
match /docs {
allow read: if existingData().users[currentUser().uid] == false;
}
match /docs/{doc} {
allow write: if currentUser().uid != null;
}
}
// MARK - Funcs ---------------
function existingData() {
return resource.data
}
function incomingData() {
return request.resource.data
}
function currentUser() {
return request.auth
}
function isSignedIn() {
return request.auth != null;
}
}
Getting error:
Listen for Query(docs where users.`40S88coPQObEWSeiYMZIJlIKJkI2` == false order by __name__) failed: Status{code=PERMISSION_DENIED, description=Missing or insufficient permissions., cause=null}
If you are using Firebase Datastore, you can simply query 1
your data:
var myDB = db.collection("YOUR-DB-COLLECTION");
var query = myDB.where("isNSFW", "==", false);
If you are using GCP datastore, you do it this other way 2:
const query = datastore
.createQuery('YOUR-COLLECTION')
.filter('isNSFW', '=', false);
Your matches are not nested correctly. They should be inside the block that starts with match /databases/{database}/documents. Also, you should strongly consider removing the match on /{document=**} because that will let everyone read any document in the database if they're logged in, ignoring all other rules.

Firebase Authentication creates user but does not add their Info to database

I am new to using firebase and ios development in general. I am having an issue with adding user's info to the firestore database even though they are being added as authenticated users. Any Suggestions?
Auth.auth().createUser(withEmail: email, password: password) { (result, err) in
if err != nil {
self.errorLabel.text = "Error Creating User"
self.errorLabel.alpha = 1
} else {
let db = Firestore.firestore()
db.collection("users").addDocument(data: ["firstname":firstname, "lastname":lastname, "uid":result!.user.uid]) { (error) in
if error != nil {
self.errorLabel.text = "error saving user data"
self.errorLabel.alpha = 1
}
}
self.transitionScreens()
}
}
}
}
Change your code to the following:
// Add a new document with a generated ID
var ref: DocumentReference? = nil
ref = db.collection("users").addDocument(data: [
"firstname": firstname,
"lastname": lastname,
"uid": result!.user.uid
]) { err in
if let err = err {
print("Error adding document: \(err)")
} else {
print("Document added with ID: \(ref!.documentID)")
}
}
Using this print statement print("Error adding document: \(err)") you can know exactly what the error is.
Also change your security rules to the following:
// Allow read/write access to all users under any conditions
// Warning: **NEVER** use this rule set in production; it allows
// anyone to overwrite your entire database.
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if true;
}
}
}
Check out the following different rules you can give access to users depending on the data
service cloud.firestore {
match /databases/{database}/documents {
//allows all users to read and write, but dangerous as any one can flood your database
match /public_collection/{document=**} {
allow read, write: if true;
}
//only read access
match /public_read_collection/{document=**} {
allow read: if true;
allow write: if false;
}
//prefered for storing users personal info, users can access only their data
match /users/{userId} {
allow read, write: if request.auth.uid == userId;
}
//any authenticated user can access or write data
match /posts/{documentId} {
allow read, write: if request.auth.uid != null;
}
}
}

Cannot read property of 'uid' of null

I need help, I used firebase as back-end and Ionic 4 as front-end.
I have error on console where when I first open/lunch the app, I received Cannot read property of 'uid' of null on console.
I read some related solutions but I haven't found one that solves my problem.
Below is the code when I implemented it in my app:
initializeApp() {
this.afAuth.auth.onAuthStateChanged(user => {
const userId = user.uid;
this.db.doc(`accounts/${userId}`).valueChanges().pipe(
take(1)
).subscribe(userData => {
const role = userData['role'];
if (role == 'USER') {
console.log('we got a customer user');
this.router.navigateByUrl('/tabs/home')
} else if (role == 'VENDOR') {
console.log('we got a seller user');
this.router.navigateByUrl('/vendor-tabs/home-vendor')
}
})
})
}
An auth state change listener is called when a user is either signed in and signed out. user will be null if not signed in. This means you have to check if user is truthy before you start accessing properties on it.
this.afAuth.auth.onAuthStateChanged(user => {
if (user) {
// user is signed in
const userId = user.uid;
}
else {
// user is signed out - can't use properties of user.
}
}
See the API documentation.
This is what I did to get rid of the cannot read property of 'uid' null.
this.afAuth.auth.onAuthStateChanged(user => {
if (user) {
const userId = user.uid;
this.db.doc(`accounts/${userId}`).valueChanges().pipe(
take(1)
).subscribe(userData => {
const role = userData['role'];
if (role == 'USER') {
console.log('we got a customer user');
this.router.navigateByUrl('/tabs/home')
} else if (role == 'VENDOR') {
console.log('we got a seller user');
this.router.navigateByUrl('/vendor-tabs/home-vendor')
}
})
} else {
return of(null);
}
})

.find() returning nothing even when data exists

I've a mongo database with 3 collections for 3 different kind of users as User,Partner,Admin. Whenever a new user of any type signup I'm searching all three collections to check if username and email exist already. I'm trying to achieve this by calling a function as:
function checkAttribute(attr,val,callback){
User.find({attr: val},function(err,user){
if(err){
console.log(err);
}else{
if(user.length === 0){
Partner.find({attr: val},function(err,partner){
if(err){
console.log(err);
}else{
if(partner.length === 0){
Admin.find({attr: val},function(err,admin){
if(err){
console.log(err);
}else{
if(admin.length === 0){
return callback(null,true);
}else{
return callback(null,false);
}
}
});
}else{
return callback(null,false);
}
}
});
}else{
return callback(null,false);
}
}
});
};
Calling function line:
checkAttribute("username",newUser.username,function(error,response){
.......
});
But this is not working as it returns true always even when users with passed username/email exists already. I am unable to find the problem. Any one knows why this is happening?
Thanks in advance.
Since you are passing in the attribute as a variable in the function parameters, the query document
{ attr: val } is an object with the key "attr", not the dynamic attribute you pass in.
To fix this, you need to use computed property names in your query object as
{ [attr]: val }
Also, the function can use async/await pattern to be more readable and for the purpose of finding if a document exist findOne does the job so
well as it returns a document if it exists and null otherwise.
So your function can be refactored as
async function checkAttribute(attr, val, callback) {
try {
const query = { [attr]: val }
const user = await User.findOne(query).exec()
const partner = await Partner.findOne(query).exec()
const admin = await Admin.findOne(query).exec()
const found = (user || partner || admin) ? true: false
return callback(null, found)
} catch (err) {
console.error(err)
return callback(err, null)
}
};
attr: in your queries will search for a db field called attr. If you want to use the function parameter attr, use [attr]: as the key.
Example:
attr = 'username'
User.find({ [attr]: val }, function (err, user) {
if (err) {
console.log(err);
}
})
This is a feature available since ES6 so should work fine. See the docs here for more info