I am referencing this tutorial for Firestore security rules. I have extracted the code from the repository and it matches that of the video.
I changed the setup code to run the firestore.rules instead of firestore-test.rules, and tried running firebase emulators:start and jest ./spec following the same directory structure, I fail the tests of "should allow delete when user is admin" and "should not allow delete for normal user" and the reason it is failing is due to the write rule in the wildcard. Does anyone know what is wrong?
collections.spec.js
const { setup, teardown } = require("./helpers");
describe("General Safety Rules", () => {
afterEach(async () => {
await teardown();
});
test("should deny a read to the posts collection", async () => {
const db = await setup();
const postsRef = db.collection("posts");
await expect(postsRef.get()).toDeny();
});
test("should deny a write to users even when logged in", async () => {
const db = await setup({
uid: "danefilled"
});
const usersRef = db.collection("users");
await expect(usersRef.add({ data: "something" })).toDeny();
});
});
describe("Posts Rules", () => {
afterEach(async () => {
await teardown();
});
test("should allow update when user owns post", async () => {
const mockData = {
"posts/id1": {
userId: "danefilled"
},
"posts/id2": {
userId: "not_filledstacks"
}
};
const mockUser = {
uid: "danefilled"
};
const db = await setup(mockUser, mockData);
const postsRef = db.collection("posts");
await expect(
postsRef.doc("id1").update({ updated: "new_value" })
).toAllow();
await expect(postsRef.doc("id2").update({ updated: "new_value" })).toDeny();
});
test("should allow delete when user owns post", async () => {
const mockData = {
"posts/id1": {
userId: "danefilled"
},
"posts/id2": {
userId: "not_filledstacks"
}
};
const mockUser = {
uid: "danefilled"
};
const db = await setup(mockUser, mockData);
const postsRef = db.collection("posts");
await expect(postsRef.doc("id1").delete()).toAllow();
await expect(postsRef.doc("id2").delete()).toDeny();
});
test("should allow delete when user is admin", async () => {
const mockData = {
"users/filledstacks": {
userRole: "Admin"
},
"posts/id1": {
userId: "not_matching1"
},
"posts/id2": {
userId: "not_matching2"
}
};
const mockUser = {
uid: "filledstacks"
};
const db = await setup(mockUser, mockData);
const postsRef = db.collection("posts");
await expect(postsRef.doc("id1").delete()).toAllow();
});
test("should not allow delete for normal user", async () => {
const mockData = {
"users/filledstacks": {
userRole: "User"
},
"posts/id1": {
userId: "not_matching1"
},
"posts/id2": {
userId: "not_matching2"
}
};
const mockUser = {
uid: "filledstacks"
};
const db = await setup(mockUser, mockData);
const postsRef = db.collection("posts");
await expect(postsRef.doc("id1").delete()).toDeny();
});
test("should allow adding a post when logged in", async () => {
const db = await setup({
uid: "userId"
});
const postsRef = db.collection("posts");
await expect(postsRef.add({ title: "new_post" })).toAllow();
});
test("should deny adding a post when not logged in", async () => {
const db = await setup();
const postsRef = db.collection("posts");
await expect(postsRef.add({ title: "new post" })).toDeny();
});
});
firestore.rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// lock down the db
match /{document=**} {
allow read: if false;
allow write: if false;
}
match /posts/{postId} {
allow update: if userOwnsPost();
allow delete: if userOwnsPost() || userIsAdmin();
allow create: if loggedIn();
}
function loggedIn() {
return request.auth.uid != null;
}
function userIsAdmin() {
return getUserData().userRole == 'Admin';
}
function getUserData() {
return get(/databases/$(database)/documents/users/$(request.auth.uid)).data
}
function userOwnsPost() {
return resource.data.userId == request.auth.uid;
}
}
}
Error trace from terminal
FirebaseError: 7 PERMISSION_DENIED:
false for 'create' # L10
● Posts Rules › should not allow delete for normal user
FirebaseError: 7 PERMISSION_DENIED:
false for 'create' # L10
at new FirestoreError (/Users/../../../../../../../../../Resources/rules/node_modules/#firebase/firestore/src/util/error.ts:166:5)
at ClientDuplexStream.<anonymous> (/Users/../../../../../../../../../Resources/rules/node_modules/#firebase/firestore/src/platform_node/grpc_connection.ts:240:13)
at ClientDuplexStream._emitStatusIfDone (/Users/../../../../../../../../../Resources/rules/node_modules/grpc/src/client.js:234:12)
at ClientDuplexStream._receiveStatus (/Users/../../../../../../../../../Resources/rules/node_modules/grpc/src/client.js:211:8)
at Object.onReceiveStatus (/Users/../../../../../../../../../Resources/rules/node_modules/grpc/src/client_interceptors.js:1311:15)
at InterceptingListener._callNext (/Users/../../../../../../../../../Resources/rules/node_modules/grpc/src/client_interceptors.js:568:42)
at InterceptingListener.onReceiveStatus (/Users/../../../../../../../../../Resources/rules/node_modules/grpc/src/client_interceptors.js:618:8)
at /Users/../../../../../../../../../Resources/rules/node_modules/grpc/src/client_interceptors.js:1127:18
I actually followed the same tutorial to get started with the firebase emulator and got the same kind of error messages. The problem for me was that when you start the simulator it automatically looks for your firestore.rules file and loads the rules. So, when you then add your mockData the rules already apply.
In order to make your test code work either change the setting for your firestore rules file in your firebase.json to a non-existing file (or rules file that allows all read/write) or add the mockData as an admin in your setup function, e.g.:
module.exports.setup = async (auth, data) => {
const projectId = `rules-spec-${Date.now()}`;
const app = firebase.initializeTestApp({
projectId,
auth
});
const db = app.firestore();
// Initialize admin app
const adminApp = firebase.initializeAdminApp({
projectId
});
const adminDB = adminApp.firestore();
// Write mock documents before rules using adminApp
if (data) {
for (const key in data) {
const ref = adminDB.doc(key);
await ref.set(data[key]);
}
}
// Apply rules
await firebase.loadFirestoreRules({
projectId,
rules: fs.readFileSync('firestore.rules', 'utf8')
});
return db;
};
Hope this helps.
Also see this question
For those that are currently having this issue firestore 8.6.1 (or equivalent), there is a bug discussed here:
https://github.com/firebase/firebase-tools/issues/3258#issuecomment-814402977
The fix is to downgrade to firestore 8.3.1, or if you are reading this in the future and firestore >= 9.9.0 has been released, upgrade to that version.
Related
I am trying to let the user only read and write their own data. My rules are as follow(from the docs)
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, update, delete: if request.auth != null && request.auth.uid == userId;
allow create: if request.auth != null;
}
}
}
The uid for my user matches my document id but i still get the error:
Uncaught Error in snapshot listener:
FirebaseError: [code=permission-denied]: Missing or
insufficient permissions.
My code for getting uid to document id
const handleSignUp = async () => {
auth
.createUserWithEmailAndPassword(email, password)
.then(async (UserCredentials) => {
const user = UserCredentials.user;
console.log("Registered with: ", user.email);
try {
const uidRef = doc(db, 'users', user.uid);
const docRef = await setDoc(uidRef, {
name: name,
age: age,
currentWeight: currentWeight,
goalWeight: goalWeight,
});
} catch (e) {
console.error("Error adding document: ", e);
}
I am really lost as I have tried many different ways and all docs / answers on here do not work for me. I am guessing the error comes when i call snapshot in this code
const getUser = async() => {
const subscriber = onSnapshot(usersRef, (snapshot) => {
let user = []
snapshot.docs.forEach((doc) => {
user.push({...doc.data(), key: doc.id })
})
setUser(user);
console.log(user);
})
return () => subscriber();
};
I am just unsure as to what is exactly wrong here. Is it my rules? My snapshot?
Given that you get a QuerySnapshot result, I suspect that your code is reading the entire users collection. But as the documentation says rules are not filters, but instead merely ensure that your code only tries to access data that it is permitted to.
So your code should only try to read the document of the currently signed in user.
const getUser = async() => {
if (getAuth().currentUser) {
const uidRef = doc(db, 'users', getAuth().currentUser.uid);
const subscriber = onSnapshot(uidRef, (doc) => {
setUser({...doc.data(), key: doc.id })
})
...
}
};
it is showing Unexpected value for STRIPE_SIGNING_SECRET error even after checking it many times in the env file
the terminal shows everything created but it does not reach firebase database I am thinking there is a error in the code
the stripe dashboard also says connected
I am using the forward to local host line in git terminal
webhook code
import { buffer } from "micro";
import * as admin from 'firebase-admin'
//secure a connection to Firebase from backend
const serviceAccount = require('../../../permissions.json');
const app = !admin.apps.length ? admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
})
: admin.app();
// establish connection to stripe
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const endpointSecret = process.env.STRIPE_SIGNING_SECRET;
if (typeof endpointSecret !== "string") {
console.error("Unexpected value for STRIPE_SIGNING_SECRET");
// potentially throw an error here
}
const fulfillOrder = async (session) => {
//console.log('Fulfilling order', session)
return app
.firestore()
.collection("user")
.doc(session.metadata.email)
.collection("orders")
.doc(session.id)
.set({
amount: session.amount_total / 100,
amount_shipping: session.amount_total_details.amount_shipping / 100,
images: JSON.parse(session.metadata.images),
timestamp: admin.firestore.FieldValue.serverTimestamp(),
})
.then(() => {
console.log(`success: order ${session.id} had been added to db`);
});
};
export default async (req, res) =>{
if(req.method === 'post'){
const requestBuffer = await buffer(req);
const payload = requestBuffer.toString();
const sig = req.headers["stripe-signature"];
let event;
// verify that the event posted came from stripe
try{
event = stripe.webhooks.constructEvent(
payload,
sig,
endpointSecret);
} catch (err) {
console.log('ERROR', err.message)
return res.status(400).send(`Webhook error: ${err.message}`)
}
//handle the checkout event
if (event.type === 'checkout.session.completed') {
const session = event .data.object;
//fulfill the order...
return fulfillOrder(session)
.then(() => res.status(200))
.catch((err) => res.status(400).send(`Webhook error: ${err.message}`));
}
}
};
export const config = {
api: {
bodyParser: false,
externalResolver: true,
},
};
firebase rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow write: if false;
allow read: if true;
}
}
}
const endpointSecret = process.env.STRIPE_SIGNNING_SECRET;
Typo: STRIPE_SIGNNING_SECRET
To avoid the next issue, fix the other typo:
const sig = req.headers["stripe-signatur"];
stripe-signature
Side-Note I connect to DB with the following code:
const mongoose = require('mongoose');
const connectDB = (url) => {
return mongoose.connect(url);
}
Problem Description:
I have two different Collections. Both Operations, findByIdAndUpdate and create must run as an atomic operation. This should be possible with mongoose Transactions.
const registerCustomer = async (req, res) => {
await CustomerRegistrationCode.findByIdAndUpdate(req.body._id, { used: true });
const customer = await Customer.create({firstName: req.body.firstName});
}
What I tried:
const registerCustomer = async (req, res) => {
const session = await mongoose.startSession();
await session.startTransaction();
try {
await CustomerRegistrationCode.findByIdAndUpdate(req.body._id, { used: true }); //updates even though
const customer = await Customer.create({ firstName: req.body.firstName });// this line will throw error
await session.commitTransaction();
session.endSession();
} catch (error) {
console.error('abort transaction');
await session.abortTransaction();
session.endSession();
throw error;
}
}
Problem The CustomerRegistrationCode Collection gets updated even though the Customer.create method throws an error. How can this be solved?
New approach to understand MongoDB Transactions fails, but this is official code from https://mongoosejs.com/docs/transactions.html
const mongoose = require('mongoose');
const debugMongo = async () => {
const db = await mongoose.createConnection("mongodb://localhost:27017/mongotest");
const Customer = db.model('Customer', new mongoose.Schema({ name: String }));
const session = await db.startSession();
session.startTransaction();
await Customer.create([{ name: 'Test' }], { session: session }); //(node:20416) UnhandledPromiseRejectionWarning: MongoServerError: Transaction numbers are only allowed on a replica set member or mongos
let doc = await Customer.findOne({ name: 'Test' });
assert.ok(!doc);
doc = await Customer.findOne({ name: 'Test' }).session(session);
assert.ok(doc);
await session.commitTransaction();
doc = await Customer.findOne({ name: 'Test' });
assert.ok(doc);
session.endSession();
}
debugMongo();
At Customer.create an error gets thrown and i don't know why. Does somebody have an minimal working example?
You are using the transaction in a wrong way, that is why it does not work.
You need to pass the session object to your operations.
const registerCustomer = async (req, res) => {
const session = await mongoose.startSession();
session.startTransaction();
try {
await CustomerRegistrationCode.findByIdAndUpdate(req.body._id, { used: true }, { session });
const customer = await Customer.create({ firstName: req.body.firstName }, { session });
await session.commitTransaction();
} catch (error) {
console.error('abort transaction');
await session.abortTransaction();
} finally {
session.endSession();
}
}
Also, I have refactored your code a bit.
You can read more about transactions here
I'm creating API tests with async-await using Supertest and Mocha.
In the accountsData.js file I created a function to generate random test accounts.
In the accountsHelper.js file I created a function to create unlimited accounts using a while loop
When I run tests on the post_accounts.js file, the first account is created successfully, but from the second account, the data generated in the accountsData.js file is already repeated.
Why isn't data randomly generated when I create more than one account using data from the accountsData.js file?
accountsData.js
const casual = require('casual');
function randomAccount() {
return {
'email': casual.email,
'password': '123456',
};
}
module.exports = {
randomAccount,
};
accountsHelper.js
const request = require('supertest');
const commonData = require('../data/commonData');
/* eslint-disable no-console */
const accountList = [];
let counterAccounts;
module.exports = {
async createAccount(account, accountsToCreate = 2, validateResponse = true) {
counterAccounts = 0;
while (counterAccounts < accountsToCreate) {
try {
const res = await request(commonData.environment.staging)
.post(commonData.endpoint.accounts)
.send(account);
if (validateResponse === true) {
if (res.status === commonData.statusCode.ok) {
accountList.push(res.body);
} else {
throw new Error('Email already exists\n\n' + JSON.stringify(res.body, null, ' '));
}
} else {
return res.body;
}
} catch (err) {
console.log(err);
}
counterAccounts++;
}
return accountList;
},
};
post_accounts.js
const accountsData = require('../../data/accountsData');
const accountsHelper = require('../../helpers/accountsHelper');
const account = accountsData.randomAccount();
describe('Create accounts with email and password', () => {
context('valid accounts', () => {
it('should create an account successfully', async() => {
const res = await accountsHelper.createAccount(account);
// eslint-disable-next-line no-console
console.log(res);
});
});
});
API response:
Create accounts with email and password
valid accounts
Error: Email already exists
{
"error": {
"statusCode": 422,
"name": "ValidationError",
"message": "The `account` instance is not valid. Details: `email` Email already exists (value: \"Lemuel.Lynch#Susan.net\").",
"details": {
"context": "account",
"codes": {
"email": [
"uniqueness"
]
},
"messages": {
"email": [
"Email already exists"
]
}
}
}
}
at Object.createAccount (/Users/rafael/Desktop/projects/services/test/helpers/accountsHelper.js:24:19)
at process._tickCallback (internal/process/next_tick.js:68:7)
[ { 'privacy-terms': false,
'created-date': '2019-08-24T10:00:34.094Z',
admin: false,
isQueued: false,
lastReleaseAttempt: '1970-01-01T00:00:00.000Z',
'agreed-to-rules': { agreed: false },
email: 'Lemuel.Lynch#Susan.net',
id: '5d610ac213c07d752ae53d91' } ]
✓ should create an account successfully (2243ms)
1 passing (2s)
The code that you posted doesn't correspond to the code that you're describing in prose.
However, I tested your accountsData.js file, in the way that your words (but not your code) say that you're using it, and it works fine.
// main.js
const { createPerson } = require(__dirname + '/accountsData')
console.log(createPerson())
console.log(createPerson())
console.log(createPerson())
console.log(createPerson())
console.log(createPerson())
Output from running it once:
$ node main.js
{ email: 'Anne_Ebert#Macie.com', password: '123456' }
{ email: 'Manley.Lindgren#Kshlerin.info', password: '123456' }
{ email: 'McClure_Thurman#Zboncak.net', password: '123456' }
{ email: 'Breitenberg.Alexander#Savannah.com', password: '123456' }
{ email: 'Keely.Mann#Stark.io', password: '123456' }
And again:
$ node main.js
{ email: 'Destany_Herman#Penelope.net', password: '123456' }
{ email: 'Narciso_Roob#gmail.com', password: '123456' }
{ email: 'Burnice_Rice#yahoo.com', password: '123456' }
{ email: 'Roma_Nolan#yahoo.com', password: '123456' }
{ email: 'Lilla_Beier#yahoo.com', password: '123456' }
Nothing in the code that you posted is actually requiring or using accountsData.js. If you change your code to use it, I think you'll see, like I do, that it works.
Problem is, you are generating the random account and storing it in a variable 'post_accounts.js(line 3)'. So, when you create an account, you are using the same payload to create multiple accounts, which obviously throws an error.
I just modified the accountHelper to properly handle your scenario. Hope this helps.
Note: The code is not tested, I just wrote it from my mind. Please test and let me know if it works.
// accountsHelper.js
const request = require('supertest');
const commonData = require('../data/commonData');
const accountsData = require('../../data/accountsData');
/* eslint-disable no-console */
const accountList = [];
module.exports = {
async createAccount(account, accountsToCreate = 1, validateResponse = true) {
// creates an array of length passed in accountsToCreate param
return (await Promise.all(Array(accountsToCreate)
.fill()
.map(async () => {
try {
const res = await request(commonData.environment.staging)
.post(commonData.endpoint.accounts)
// takes account if passed or generates a random account
.send(account || accountsData.randomAccount());
// validates and throw error if validateResponse is true
if (validateResponse === true && (res.status !== commonData.statusCode.ok)) {
throw new Error(
'Email already exists\n\n' +
JSON.stringify(res.body, null, ' ')
);
}
// return response body by default
return res.body;
} catch (e) {
console.error(e);
// return null if the create account service errors out, just to make sure the all other create account call doesnt fail
return null;
}
})))
// filter out the null(error) responses
.filter(acc => acc);
}
};
//post_accounts.js
const accountsHelper = require('../../helpers/accountsHelper');
const accountsData = require('../../data/accountsData');
const GENERATE_RANDOM_ACCOUNT = null;
describe('Create accounts with email and password', () => {
context('valid accounts', () => {
it('should create an account successfully', async () => {
const result = await accountsHelper.createAccount();
expect(result.length).toEquals(1);
});
it('should create 2 accounts successfully', async () => {
const result = await accountsHelper.createAccount(GENERATE_RANDOM_ACCOUNT, 2);
expect(result.length).toEquals(2);
});
it('should not create duplicate accounts', async () => {
const account = accountsData.randomAccount();
// here we are trying to create same account twice
const result = await accountsHelper.createAccount(account, 2);
// expected result should be one as the second attempt will fail with duplicate account
expect(result.length).toEquals(1);
});
});
});
I have the following Firebase Function that makes use of Auth0 to get a user profile.
'use strict';
const {
dialogflow,
Image,
} = require('actions-on-google')
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
const db = admin.firestore();
// database collection and key names
const DB_BANK_COLLECTION_KEY = 'bank'
// the action name from all Dialogflow intents
const INTENT_WELCOME_USER = 'Default Welcome Intent';
// Initialize the Auth0 client
var AuthenticationClient = require('auth0').AuthenticationClient;
var auth0 = new AuthenticationClient({
domain: functions.config().familybank.auth0.domain,
clientID: functions.config().familybank.auth0.clientid
});
const app = dialogflow();
app.intent(INTENT_WELCOME_USER, async (conv) => {
console.log('Request: ' + JSON.stringify(conv.request));
const userInfo = await auth0.getProfile(conv.user.access.token)
.catch( function(err) {
console.error('Error getting userProfile from Auth0: ' + err);
conv.close("Something went wrong. Please try again in a few minutes. " + err)
});
console.log('userInfo: ' + JSON.stringify(userInfo));
// check for existing bank, if not present, create it
var bankRef = db.collection(DB_BANK_COLLECTION_KEY).doc(userInfo.email);
const bankSnapshot = await bankRef.get()
})
exports.accessAccount = functions.https.onRequest(app);
I tried to mock auth0 in my tests using the following code (and several permutations), but the actual function always gets called instead of the mock.
const chai = require('chai');
const assert = chai.assert;
const sinon = require('sinon');
// Require firebase-admin so we can stub out some of its methods.
const admin = require('firebase-admin');
const test = require('firebase-functions-test')();
var AuthenticationClient = require('auth0').AuthenticationClient;
var auth0 = new AuthenticationClient({
domain: "mock",
clientID: "mock"
});
describe('Cloud Functions', () => {
let myFunctions, adminInitStub;
before(() => {
test.mockConfig({"familybank": {"auth0": {"domain": "mockdomain", "clientid": "mockid"}}});
adminInitStub = sinon.stub(admin, 'initializeApp');
sinon.stub(admin, 'firestore')
.get(function() {
return function() {
return "data";
}
});
sinon.stub(auth0, 'getProfile').callsFake( function fakeGetProfile(accessToken) {
return Promise.resolve({"email": "daniel.watrous#gmail.com", "accessToken": accessToken});
});
myFunctions = require('../index');
});
after(() => {
adminInitStub.restore();
test.cleanup();
});
describe('accessAccount', () => {
it('should return a 200', (done) => {
const req = {REQUESTDATA};
const res = {
redirect: (code, url) => {
assert.equal(code, 200);
done();
}
};
myFunctions.accessAccount(req, res);
});
});
})
Is there some way to mock auth0 for my offline tests?
I discovered that rather than initialize the Auth0 AuthenticationClient, I could first require the UsersManager, where the getProfile (which wraps getInfo) is defined.
var UsersManager = require('auth0/src/auth/UsersManager');
In my before() method, I can then create a stub for getInfo, like this
sinon.stub(UsersManager.prototype, 'getInfo').callsFake( function fakeGetProfile() {
return Promise.resolve({"email": "some.user#company.com"});
});
All the calls to auth0.getProfile then return a Promise that resolves to the document shown in my stub fake function.