I am attempting to read a clan only if the users uid is a document in the subcollection members but i cant seem to get it to work just more permission errors.
match /clans/{clanName} {
allow create;
allow read: if exists(/databases/$(database)/documents/clans/{clanName}/members/$(request.auth.uid))
match /members/{uid} {
allow create,write,delete: if uid == request.auth.uid;
}
}
The code that is calling this is using a reference stored in another document
this.getProfile
.data()
.clan.get());
which equates to
firebase
.firestore()
.collection("clans")
.doc(clanName)
.get()
leaveClan() {
console.log(this.getUser.displayName + " leaving " + this.getProfile);
console.log(this.getProfile.data().clan.get().collection("members")
.doc(this.getUser.uid));
this.getProfile
.clan
.get()
.collection("members")
.doc(this.getUser.uid)
.delete()
.then(() => {
firebase
.firestore()
.collection("profiles")
.doc(this.getUser.uid)
.update({
clan: null
});
});
},
The way your embedding {clanName} in the get() call is incorrect. Variables are all in $(variablename) format, similar to how you already have $(database) and $(request.auth.uid).
So something like:
if exists(/databases/$(database)/documents/clans/$(clanName)/members/$(request.auth.uid))
Also see:
The Firebase documentation on accessing other documents in security rules.
Related
I have the following rules for a Firestore database:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{user} {
allow create, update: if request.auth != null;
allow read, delete, list: if request.auth.token.email == resource.data.email;
}
}
}
In my app I can read the data by using getDocs(), but even though the same security is on the delete function, the following does not work:
const removeUser = async(number) => {
console.log(number);
console.log(auth.currentUser?.email || "No User");
const usersRef = collection(db, 'users');
const qU = query(usersRef, where('number', '==', number));
const usersQuerySnapshot = await getDocs(qU);
console.log(usersQuerySnapshot.docs.map(d => d.data().email);
usersQuerySnapshot.forEach((user) => {
deleteDoc(doc);
});
};
After some debugging, I know that deleting the deleteDoc() lines still gives me an insufficient permissions error.
This function is called from another function, update(). This function also calls a getUsersOfTeacher() (a teacher has its own users) which uses the same code as removeUser() but without the deleting part. This function works with no errors.
Does anyone know what's going on here?
Firebase's security rules do not filter the data. Instead they merely ensure that the code doesn't request more data than it's authorized for.
Since your rules require that the user's email address is in the email field of the document, your query must do the same. So something like:
query(
usersRef,
where('number', '==', number),
where('email', '==', auth.currentUser?.email) // 👈
);
I am trying to build callable cloud functions, when users delete a post, it also try to delete the comments, which is a sub-collection of the post. so I saw the example and implemented just like a documentation example
const admin = require('firebase-admin');
const firebase_tools = require('firebase-tools');
const functions = require('firebase-functions');
admin.initializeApp({
serviceAccountId: 'xxxxxx-xxxxx#appspot.gserviceaccount.com'
}
);
exports.mintAdminToken = functions.https.onCall(async (data: any, context: any) => {
const uid = data.uid;
const token = await admin
.auth()
.createCustomToken(uid, { admin: true });
return { token };
});
exports.recursiveDelete = functions
.runWith({
timeoutSeconds: 540,
memory: '2GB'
})
.https.onCall(async (data: any, context: any) => {
// Only allow admin users to execute this function.
if (!(context.auth && context.auth.token && context.auth.token.admin)) {
throw new functions.https.HttpsError(
'permission-denied',
'Must be an administrative user to initiate delete.'
);
}
const path = data.path;
console.log(
`User ${context.auth.uid} has requested to delete path ${path}`
);
await firebase_tools.firestore
.delete(path, {
project: process.env.GCLOUD_PROJECT,
recursive: true,
yes: true,
token: functions.config().fb.token
});
return {
path: path
};
});
and I succeeded in receiving the custom token to the client. but what I have to do now? after getting token I called the "recursiveDelete" function from client but it occurs error PERMISSION_DENIED
Should the user who received the token be initialized with a new custom admin token? (if I misunderstand let me know)
Is the admin token really necessary when deleting a sub collection like this? It's difficult to use, so I ask.
I don't think that you really need a custom token for this use case and I suggest that you use firebase firestore rules rather than implementing your own role based authentication.
Steps to follow:
1- create a collection that you may call "users" and include in it a field of the role that this user may have such as "ADMIN". every document id in this collection can be the auth uid of users that firebase auth generates. you can get this uid from your frontend by using the currentUser prop and it's all explained here
2- protect your database with firestore rules as such:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// only admins can remove posts
match /posts/{postID} {
allow read, write: if isAdmin();
}
// only admins can remove comments
match /comments/{commentID} {
allow read, write: if isAdmin();
}
// this function will check if the caller has an admin role and allow or disallow the task upon that
function isAdmin() {
return get(/databases/$(database)/documents/
users/$(request.auth.uid)).data.role == "ADMIN";
}
}
}
3- after you succefully deletes a post document you can create a function with onDelete trigger that get invoked and deletes the comments subcollection recursivley and to do that you should include this bit of code:
const client = require('firebase-tools');
exports.recursiveDelete = functions.firestore
.document('posts/{postID}')
.onDelete((snap, context) => {
.....
await client.firestore
.delete(collectionPath, {
project: process.env.GCLOUD_PROJECT,
recursive: true,
yes: true
});
}
I am trying to leverage a reference data type from the profile document to access a subcollection on the referenced clan document.
leaveClan() {
console.log(this.getUser.displayName + " leaving " + this.getProfile);
console.log(this.getProfile.data().clan);
console.log(this.getProfile.data().clan.get().collection("members")
.doc(this.getUser.uid));
firebase
.firestore().
this.getProfile.data()
.clan
.get()
.collection("members")
.doc(this.getUser.uid)
.delete()
.then(() => {
firebase
.firestore()
.collection("profiles")
.doc(this.getUser.uid)
.update({
clan: null
});
});
},
Ok figured it out.
this.getProfile
.data()
.clan.get()
.then(snapshot => {
console.log(snapshot.data());
});
just .get() or whatever normal firebase operation you would do. The field with the reference is essentially the doc reference as well.
I am sure there is a better answer that explains why this works or the right terms but it works haha.
Using flutter, I have installed the firebase-auth and firestore packages and am able to both authenticate with firebase auth and make a call into firestore as long as I don't have any rules around the user.
I have a button that calls _handleEmailSignIn and I do get a valid user back (since they are in the Firebase Auth DB)
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
final FirebaseAuth _auth = FirebaseAuth.instance;
void _handleEmailSignIn(String email, String password) async {
try {
FirebaseUser user = await _auth.signInWithEmailAndPassword(
email: email, password: password);
print("Email Signed in " + user.uid); // THIS works
} catch (err) {
print("ERROR CAUGHT: " + err.toString());
}
}
I then have another button that calls this function to attempt to add a record into the testing123 collection.
Future<Null> _helloWorld() async {
try {
await Firestore.instance
.collection('testing123')
.document()
.setData(<String, String>{'message': 'Hello world!'});
print('_initRecord2 DONE');
} catch (err) {
print("ERROR CAUGHT: " + err.toString());
}
}
Now this works as long as I don't have any rules around checking the request user. This works...
service cloud.firestore {
match /databases/{database}/documents {
match /testing123auth/{doc} {
allow read, create
}
}
}
This does not which gives PERMISSION_DENIED: Missing or insufficient permissions. when I want to make sure I have the authenticated user I did with _handleEmailSignIn.
service cloud.firestore {
match /databases/{database}/documents {
match /testing123auth/{doc} {
allow read, create: if request.auth != null;
}
}
}
I suspect that the firestore request is not including the firebase user. Am I meant to configure firestore to include the user or is this supposed to be automatic as part of firebase?
One thing to note that's not well documented is that firebase_core is the "Glue" that connects all the services together and when you're using Firebase Authentication and other Firebase services, you need to make sure you're getting instances from the same firebase core app configuration.
final FirebaseAuth _auth = FirebaseAuth.instance;
This way above should not be used if you're using multiple firebase services.
Instead, you should always get FirebaseAuth from FirebaseAuth.fromApp(app) and use this same configuration to get all other Firebase services.
FirebaseApp app = await FirebaseApp.configure(
name: 'MyProject',
options: FirebaseOptions(
googleAppID: Platform.isAndroid ? 'x:xxxxxxxxxxxx:android:xxxxxxxxx' : 'x:xxxxxxxxxxxxxx:ios:xxxxxxxxxxx',
gcmSenderID: 'xxxxxxxxxxxxx',
apiKey: 'xxxxxxxxxxxxxxxxxxxxxxx',
projectID: 'project-id',
bundleID: 'project-bundle',
),
);
FirebaseAuth _auth = FirebaseAuth.fromApp(app);
Firestore _firestore = Firestore(app: app);
FirebaseStorage _storage = FirebaseStorage(app: app, storageBucket: 'gs://myproject.appspot.com');
This insures that all services are using the same app configuration and Firestore will receive authentication data.
There shouldn't be any special configuration needed for the firestore to do this.
This is all you should need.
Modified from Basic Security Rules:
service cloud.firestore {
match /databases/{database}/documents {
match /testing123/{document=**} {
allow read, write: if request.auth.uid != null;
}
}
}
It seems they check if the uid is null rather than the auth itself. Try this out and see if it works. Also, it seemed that your code was inconsistent as the firestore rule had testing123auth and flutter had testing123. I'm not sure if that was intentional.
to check if the user is signed in you should use
request.auth.uid != null
I would have suggested to make the rule like:
service cloud.firestore {
match /databases/{database}/documents {
match /testing123auth/{documents=**} {
allow read, create: if true;
}
}
}
Or, better yet, limit the scope of the user:
service cloud.firestore {
match /databases/{database}/documents {
match /testing123auth/{userId} {
allow read, create:
if (request.auth.uid != null &&
request.auth.uid == userId); // DOCUMENT ID == USERID
} // END RULES FOR USERID DOC
// IF YOU PLAN TO PUT SUBCOLLECTIONS INSIDE DOCUMENT:
match /{documents=**} {
// ALL DOCUMENTS/COLLECTIONS INSIDE THE DOCUMENT
allow read, write:
if (request.auth.uid != null &&
request.auth.uid == userId);
} // END DOCUMENTS=**
} // END USERID DOCUMENT
}
}
firebase.initializeApp(config);
const db = firebase.firestore();
const googleSignIn = async () => {
return await Expo.Google.logInAsync({
androidClientId,
iosClientId,
scopes: ['profile', 'email'],
});
};
const firebaseLogin = async (accessToken) => {
const cred = firebase.auth.GoogleAuthProvider.credential(null, accessToken);
await firebase.auth().signInWithCredential(cred).catch(console.error);
const idToken = await firebase.auth().currentUser.getIdToken(true).catch(console.error);
};
await firebaseLogin(googleSignIn().accessToken);
db.collection("any").doc().set({test: "OK"})
I get a permission denied error when trying to write to Firestore using a request.auth.uid != null; security rule, but when I replace it with true it works.
It seems that the Firestore component of the web SDK does not send authentication details, even though the API on the client reports Firebase is logged in, and the user last login date appears in the web GUI.
Do I need to pass authentication details to the Firestore component when logging in directly with Google (instead of using the Firebase login APIs)?
The code is running in a React Native app via Expo.
Another example that gets a permission denied:
firebase.auth().onAuthStateChanged((user) => {
if (user) {
firebase.firestore().collection("any").doc().set({test: "OK"});
}
});
Rules
// This is OK:
service cloud.firestore {
match /databases/{database}/documents {
match /any/{doc} {
allow read, write: if true;
}
}
}
// Permission denied
service cloud.firestore {
match /databases/{database}/documents {
match /any/{doc} {
allow read, write: if request.auth.uid != null;
}
}
}
Related
Firebase Firestore missing or insufficient permissions using Expo (React Native)
https://forums.expo.io/t/firestore-with-firebase-auth-permissions-to-read-write-only-to-signed-in-users/5705
This solution, and possibly this whole issue, may be specific to React Native.
In a similar question, Jade Koskela noticed that requests were missing the Origin header, and applies a patch to React's XHRInterceptor to work around the missing auth object:
const xhrInterceptor = require('react-native/Libraries/Network/XHRInterceptor');
xhrInterceptor.setSendCallback((data, xhr) => {
if(xhr._method === 'POST') {
// WHATWG specifies opaque origin as anything other than a uri tuple. It serializes to the string 'null'.
// https://html.spec.whatwg.org/multipage/origin.html
xhr.setRequestHeader('Origin', 'null');
}
});
xhrInterceptor.enableInterception();