query permissions for custom token - firebase

I have a custom token that contains a payload of appid to identify third party apps.
I'd like to verify reads/writes to my data such that:
the user is signed in (there is a valid uid / token)
the appid is registered in the /apps collection
the uid and appid are fields in the record and match the credentials
(unrelated to this question, but to be complete) this record matches the schema for the document.
current best solution and remaining questions
This answer, that I eventually stumbled upon, is pretty good, but it might be improved.
The first thing I had to do was correctly identify the payload I was giving in the custom token -- because I am using a cloud service function to generate the payload (graphcool), my payload was default: { appid } instead of just appid. From there, just some rewording on the permissions was enough to succeessdully validate using the current rules I'd imagined:
service cloud.firestore {
match /databases/{database}/documents {
match /sampleData/{type}/{appName}/{record} {
allow read: if isSignedIn() && isValidApp(database) && ownsExisting() && appIdInExisting()
allow write: if isSignedIn() && isValidApp(database) && ownsPayload() && appIdInPayload()
}
// functions
function isSignedIn () {
return request.auth != null
}
function isValidApp (database) {
return exists(/databases/$(database)/documents/apps/$(request.auth.token.appid))
}
function ownsExisting () {
return resource.data.uid == request.auth.uid
}
function ownsPayload () {
return request.resource.data.uid == request.auth.uid
}
function appIdInExisting () {
return resource.data.app_id == request.auth.token.appid
}
function appIdInPayload () {
return request.resource.data.app_id == request.auth.token.appid
}
}
}
Someone could do better though.
is there any way to validate the appid without using an exists request (And without writing a if-else if chain) -- like with an array directly in the rules perhaps?
how can I ensure that the appid specified in the payload matches the one I envisioned when I issued the service credentials to the client third-party-app?
edit: original question
I thought I could get at least the first two myself:
service cloud.firestore {
match /databases/{database}/documents {
match /sampleData {
allow read: if isSignedIn() && isValidApp() && ownsExisting() && appIdInExisting()
allow write: if isSignedIn() && isValidApp() && ownsPayload() && appIdInPayload()
}
// functions
function isSignedIn () {
return request.auth != null
}
function isValidApp () {
return get(path('apps')).data.child(request.auth.token.appid).exists()
}
function ownsExisting () {
return resource.data.uid == request.auth.uid
}
function ownsPayload () {
return request.resource.data.uid == request.auth.uid
}
function appIdInExisting () {
return resource.data.app_id == request.auth.token.appid
}
function appIdInPayload () {
return request.resource.data.app_id == request.auth.token.appid
}
}
}
/apps has 1 document called "sample-app-id" with id and name fields of "sample-app-id" ... but using this in my token does not work: FirebaseError: Missing or insufficient permissions
I am generating the token via this function on my server:
var FirebaseAdmin = require('firebase-admin')
var serviceAccount = require('./firebase-service-credentials.json')
var claims = require('./custom-token-claims') // {appid: 'sample-app-id'}
let credential = FirebaseAdmin.credential.cert(serviceAccount)
FirebaseAdmin.initializeApp({ credential })
const generateTokenWithPayload = async id => {
try {
const token = await FirebaseAdmin.auth().createCustomToken(id, claims)
return { data: { token } }
} catch (err) {
return { error }
}
}
module.exports = async event =>
await generateTokenWithPayload(event.data.userIdentifier)
and before posting I am signing in -- this part I can verify seems to be working as I see the new, non-anonymous user in my Authentication -> Users tab in the firebase console:
— Feb 11, 2019 Feb 11, 2019 smaple-user-id
Here's essentially the client code:
await firebase
.auth()
.signInWithCustomToken(token)
.catch(console.error)
const db = await firebase.firestore()
db.collection(path + this.state.appName).add(payload)
I am posting a record with the schema {app_id, app_name, date, metric, uid} to sampleData/metrics/sample-app-name/{auto-generated}
notes:
the number of apps that are going to be registered is small -- it would probably make sense from a financial perspective to make this just a static array in the permissions file, if that is possible, rather than a get request.
big improvement - I just noticed that request.auth.token.appid should be request.auth.token.default.appid because I was using export defaultinstead of modules.export

This answer is pretty good, but it might be improved.
The first thing I had to do was correctly identify the payload I was giving in the custom token -- because I am using a cloud service function to generate the payload (graphcool), my payload was default: { appid } instead of just appid. From there, just some rewording on the permissions was enough to succeessdully validate using the current rules I'd imagined:
service cloud.firestore {
match /databases/{database}/documents {
match /sampleData/{type}/{appName}/{record} {
allow read: if isSignedIn() && isValidApp(database) && ownsExisting() && appIdInExisting()
allow write: if isSignedIn() && isValidApp(database) && ownsPayload() && appIdInPayload()
}
// functions
function isSignedIn () {
return request.auth != null
}
function isValidApp (database) {
return exists(/databases/$(database)/documents/apps/$(request.auth.token.appid))
}
function ownsExisting () {
return resource.data.uid == request.auth.uid
}
function ownsPayload () {
return request.resource.data.uid == request.auth.uid
}
function appIdInExisting () {
return resource.data.app_id == request.auth.token.appid
}
function appIdInPayload () {
return request.resource.data.app_id == request.auth.token.appid
}
}
}
Someone could do better though.
is there any way to validate the appid without using an exists request (And without writing a if-else if chain) -- like with an array directly in the rules perhaps?
how can I ensure that the appid specified in the payload matches the one I envisioned when I issued the service credentials to the client third-party-app?

Related

How to grant collection access to user based in other collection?

Let's assume that there is 3 collections and they are at the same hierarchy level:
User
UserAndOtherCollectionRelationship
OtherCollection
I desire to grant access on "OtherCollection" records to the users that own that record or are related to it (only read access).
Understand "UserAndOtherCollectionRelationship" as
UserAndOtherCollectionRelationship: {
'userId': uid, //user id provided by Firebase Auth Service
'otherCollectionId': 000,
'roles': ['owner', 'reader', ...]
}
This is what I have:
match /databases/{database}/documents {
match /otherCollection/{otherCollectionId} {
allow read, update, delete: if(isOtherCollectionOwner());
allow create: if(isSignedIn());
}
match /user/{userId} {
allow read, write: if(isSignedIn() && isUserOwner(userId));
}
match /userAndOtherCollectionRelationship/{userAndOtherCollectionRelationshipId} {
allow read: if(resource.data.userId == request.auth.uid && isSignedIn());
allow create: if(isSignedIn());
allow update, delete: if(resource.data.userId == request.auth.uid);
}
// Functions
function isSignedIn() {
return request.auth != null;
}
function isUserOwner(userId) {
return request.auth.uid == userId;
}
function isOtherCollectionOwner() {
return isUserOwner(getUserAndOtherCollectionRelationshipData().userId) && getOtherCollectionData().roles.hasAll(['owner']);
}
//This is the function that I believe that it's not working propertly
function getuserAndOtherCollectionRelationshipData() {
return get(/databases/$(database)/documents/userAndOtherCollectionRelationship/{document=**}).data;
}
}
Considering that the client (the app) must create a filter (where clause) to get only the desired records, I could not find a way to do that with this schema too.
So I put the user roles as a field on the "otherCollection" record:
otherCollection: {
...,
'userAndRoles': {
'replaceByUID': ['owner', ...]
},
}
updated the security rule function to:
function isOtherCollectionOwner() {
return get(/databases/$(database)/documents/OtherCollection/$(otherCollectionId)).data.roles[request.auth.uid].hasAll(['owner']);
}
Here is the client call:
final querySnapshot = await firestore.collection('otherCollection')
.where('user.$userId', arrayContains: 'owner')
.where('otherCollectionId', whereIn: otherCollectionIdList)
.get();
What is the best solution?
Change the data model to...
Set a different security rule as...
When a user wants to access an another collection, we have to set a rule under that collection.
So when you create a document in a collection, you have to create the same id in the other collection and create a field called owner.
This field owner contains the uid of the person who created the document.
userAndOtherCollectionRelationshipId === otherCollectionId
UserAndOtherCollectionRelationship: {
'owner': uid
}
In this way, when a user try to read the document, we check if he is owner or not with isOwner(otherCollectionId, request.auth.uid) function with Collectionid, and the userId. In the function you check if the owner who created the document is the same who is trying to read the document.
You can do create a rule as following:
match /otherCollection/{otherCollectionId} {
allow read, update, delete: if isOwner(otherCollectionId, request.auth.uid);
allow create: if isSignedIn();
}
function isOwner(docId, userId) {
return get(/databases/$(database)/documents/userAndOtherCollectionRelationship/$(docId)).data.owner == userId;
}
To solve the issue, I updated the data model removing the userAndOtherCollectionRelationship collection and add the owner attribute to the otherCollection.
Any other relationship would be added as an attribute to otherCollection.
So the otherCollection looks like this now:
otherCollection: {
owner: ["user_uid", "other_user_id"],
..., //other atributes
}
The security rules were updated to:
match /otherCollection/{otherCollectionId} {
allow read, update, delete: if(isOtherCollectionOwner());
allow create: if(isSignedIn());
}
function isOtherCollectionOwner() {
return ([request.auth.uid] in (resource.data.owner));
}
The security rules tests were updated to:
const myAuth = {uid: 'my_user_uid', email: 'my#mail.com'};
const MY_PROJECT_ID = "my_project_id";
function getAdminFirestore() {
return firebase.initializeAdminApp({projectId: MY_PROJECT_ID, auth: myAuth}).firestore();
}
function getFirestore(auth) {
return firebase.initializeTestApp({projectId: MY_PROJECT_ID, auth: auth}).firestore();
}
describe("MyApp", () => {
it("Can list if is owner", async () => {
const admin = getAdminFirestore();
const setupOtherCollection = admin.collection('otherCollection').doc('otherCollectionId');
await setupOtherCollection.set({'name': 'myOtherCollection', 'owner': [myAuth.uid]});
const db = getFirestore(myAuth);
const otherCollectionCollection = db.collection("otherCollection").where("owner", "array-contains", [myAuth.uid]);
await firebase.assertSucceeds(otherCollectionCollection.get());
});
});

Firestore rules are not accepting custom claims from Stripe (stripeRole)

I'm using firebase-stripe-extension, and I can't get firestore rules to work with custom claims from stripe extension I get this error: FirebaseError: Missing or insufficient permissions.
Is it because the token was not refreshed?
(If I retry after some time it works) => Made me pull my hair out :)
Here's my code:
function isPro() {
return request.auth.token.stripeRole == "pro";
}
function isProPlus() {
return request.auth.token.stripeRole == "proplus";
}
function proPlusServicesOK() {
return getUserData().serviceCount < 5;
}
function proServicesOK() {
return getUserData().serviceCount < 1;
}
function uidMatches() {
return request.auth.uid == resource.data.userId;
}
match /services/{id} {
allow create: if isPro() && proServicesOK() || isProPlus() && proPlusServicesOK();
allow update: if uidMatches();
allow delete: if uidMatches() || isAdmin();
}
And the response from cloud functions with the stripeRole:
Obviously, it was an issue with the token not refreshed, forcing token refresh fixed it!
this.afAuth.currentUser.then((user) => {
user.getIdToken(true);
});

Error: Missing or insufficient permissions

I got a firestore like this:
:stores
|
$Store
:orders
|
$Order
:items
I want to read orders from my database using a user having an workerUid same as the request.auth.uid but geht the Error: Missing or insufficient permissions.
The important part of my firebase rules:
service cloud.firestore {
match /databases/{database}/documents {
//Matches any document in the stores collection
match /stores/{store} {
function isStoreAdmin(uid) {
return get(/databases/stores/$(store)).data.adminUid == uid;
}
function isStoreWorker(uid) {
return get(/databases/stores/$(store)).data.workerUid == uid;
}
allow read: if request.auth.uid != null;
allow write: if request.auth.uid == resource.data.adminUid;
//Matches any document in the orders collection
match /orders/{document=**} {
allow read, write: if isStoreAdmin(request.auth.uid) || isStoreWorker(request.auth.uid);
}
}
}
}
Funny thing is, that it works if I do this:
match /orders/{document=**} {
allow read, write: if isStoreWorker(request.auth.uid);
}
or this:
match /orders/{document=**} {
allow read, write: if request.aut.uid != null;
}
When deploying the rules I get no syntax error so I really can't understand why this is not working. Does anyone have any ideas? Thank you so much!
Edit:
function readAllDocuments(collectionReference, callback,finishedCallback){
collectionReference.get().then(function(querySnapshot) {
querySnapshot.forEach(function(doc) {
callback(doc.id,doc.data());
});
finishedCallback();
});
}
const storeDocument = getRootCollection(STORES_COLLECTION_ID).doc(storeId);
const orderCollection = storeDocument.collection(STOREORDERS_COLLECTION_ID);
orders=new Map();
readAllDocuments(orderCollection, function (id, data) {
orders.set(id,data);
},function(){
finishedLoading();
});
The documentation for use of get() in a security rule states:
...the path provided must begin with /databases/$(database)/documents
Make these changes to the get() paths:
function isStoreAdmin(uid) {
return get(/databases/$(database)/documents/stores/$(store)).data.adminUid == uid;
}
function isStoreWorker(uid) {
return get(/databases/$(database)/documents/stores/$(store)).data.workerUid == uid;
}

firestore batch.commit() succeeds but doc.get() fails [duplicate]

Using get() in Firestore rules on a newly created document causes the return value to be false. If you wait a few seconds and hit a security rule that calls get() on that same new document, get() will then return the expected value. Am I missing something in my rules and/or code, or is this a bug with Firestore?
service cloud.firestore {
match /databases/{database}/documents {
match /budgets/{budgetId} {
allow read: if resource.data.userId == request.auth.uid;
allow create: if request.auth.uid == request.resource.data.userId;
match /accounts/{accountId} {
allow read, create, update: if userOwnsBudget(); // <--- failing for newly created budget documents
}
function userOwnsBudget() {
return get(/databases/$(database)/documents/budgets/$(budgetId)).data.userId == request.auth.uid;
}
}
}
}
const data: Budget = {
userId: userId,
budgetName: budgetName,
currencyType: currencyType
};
try {
const newBudget = await this.afs.collection<Budget>('budgets').add(data);
const accountsCollection: AngularFirestoreCollection<BudgetAccount> = this.afs.collection<BudgetAccount>('budgets/' + newBudget.id + '/accounts');
//Insufficient permission, but occasionally succeeds
accountsCollection.valueChanges().subscribe(accounts => {
console.log(accounts);
});
setTimeout(() => {
accountsCollection.valueChanges().subscribe(accounts => {
console.log(accounts)
});
}, someArbitaryTime) // Typically waiting 5 seconds is enough, but occasionally that will still fail
} catch(error) {
console.error(error);
}
EDIT: This bug has been resolved.
This is unfortunately a known issue at the moment. We're working on a fix and will update here once it's resolved. Thanks and sorry!

Unable to get() data from Firestore immediately after creation [duplicate]

Using get() in Firestore rules on a newly created document causes the return value to be false. If you wait a few seconds and hit a security rule that calls get() on that same new document, get() will then return the expected value. Am I missing something in my rules and/or code, or is this a bug with Firestore?
service cloud.firestore {
match /databases/{database}/documents {
match /budgets/{budgetId} {
allow read: if resource.data.userId == request.auth.uid;
allow create: if request.auth.uid == request.resource.data.userId;
match /accounts/{accountId} {
allow read, create, update: if userOwnsBudget(); // <--- failing for newly created budget documents
}
function userOwnsBudget() {
return get(/databases/$(database)/documents/budgets/$(budgetId)).data.userId == request.auth.uid;
}
}
}
}
const data: Budget = {
userId: userId,
budgetName: budgetName,
currencyType: currencyType
};
try {
const newBudget = await this.afs.collection<Budget>('budgets').add(data);
const accountsCollection: AngularFirestoreCollection<BudgetAccount> = this.afs.collection<BudgetAccount>('budgets/' + newBudget.id + '/accounts');
//Insufficient permission, but occasionally succeeds
accountsCollection.valueChanges().subscribe(accounts => {
console.log(accounts);
});
setTimeout(() => {
accountsCollection.valueChanges().subscribe(accounts => {
console.log(accounts)
});
}, someArbitaryTime) // Typically waiting 5 seconds is enough, but occasionally that will still fail
} catch(error) {
console.error(error);
}
EDIT: This bug has been resolved.
This is unfortunately a known issue at the moment. We're working on a fix and will update here once it's resolved. Thanks and sorry!

Resources