Would like suggestions related to a many to many structure with roles/permissions.
We need a structure that permits users to belong to many organizations and users have a role/permissions for each organization. For example, User1 belongs to ABC CO as Admin, and User1 belongs to XYZ CO as Guest
We have solved this issue as follows:
organizations (collection) {
ABC (doc) {
permissions (object): {
User1DocID (object): {
admin: true
}
}
}
XYZ (doc) {
permissions (object): {
User2DocID (object): {
guest: true
}
}
}
}
This way you can configure the rules like this:
match /origanizations/{origanization} {
allow update, read: if resource.data.permissions[request.auth.uid].admin == true;
allow read: if resource.data.permissions[request.auth.uid].guest == true;
}
For the resources of the organization you would have to store the Organization ID in the specific docs (obviously). Then you can setup the rules for them as follows:
match /origanizationRessources/{origanizationRessource} {
allow update: if get(/databases/$(database)/documents/organizations/$(resource.data.organizationId)).data.permissions[request.auth.uid].admin == true;
}
You can also easily query for data that the user has specific permissions on with this design.
Please note: This design fits our purposes as we have a finite, straightforward number of users assigned to the organizations. If you are unsure, have a look at the limits in terms of document sizes (see https://firebase.google.com/docs/firestore/quotas) to find out whether you have to rely on another design. If you happen to be in the position of potentially hitting those limits, consider a seperate mapping collection.
Related
I am having an issue with Firestore rules when the permission is stored in another document in another collection. I haven't been able to find any examples of this, but I have read that it can be done.
I want to do it this way to avoid having to do a lot of writes when a student shares his homework list with many other students. Yes, I know this counts as another read.
I have 3 collections, users, permissions, and homework along with some sample data.
users
{
id: fK3ddutEpD2qQqRMXNW,
name: "Steven Smith"
},
{
id: YI2Fx656kkkk25,
name: "Becky Kinsley"
},
{
id: CAFDSDFR244bb,
name: "Tonya Benz"
}
permissions
{
id: fK3ddutEpD2qQqRMXNW,
followers: [YI2Fx656kkkk25,CAFDSDFR244bb]
}
homework
{
id: adfsajkfsk4444,
owner: fK3ddutEpD2qQqRMXNW,
name: "Math Homework",
isDone: false
}
The start of my firestore rules:
service cloud.firestore {
//lock down the entire firestore then open rules up.
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if false;
}
match /homework/{ } {
allow get: if isSignedIn()
}
// helper functions
function isSignedIn() {
return request.auth != null;
}
function isUser(userId) {
return request.auth.uid == userId;
}
function isOwner(userId) {
return request.auth.uid == resource.data.uid;
}
}
}
Use case:
Steven Smith Shared his homework list with Tonya Benz.
Tonya Benz is logged into the app to view her friend Steven's homework. The app runs this query to get the homework list of Steven Smith.
var homeworkRef = db.collection("homework");
var query = homeworkRef.where("owner", "==", "fK3ddutEpD2qQqRMXNW");
Question:
What is the proper Firestore match rule that takes the "owner" field from the homework collection to look up it up as the id in the permissions collection when the user Tonya Benz is signed in so this query can run.
With your current query and database structure, you won't be able to achieve your goal using security rules.
Firstly, it sounds like you're expecting to be able to filter the results of the query based on the contents of another document. Security rules can't act as query filters. All the documents matched by the query must be granted read access by security rules, or the entire query is denied. You will need to come up with a query that is specific about which documents should be allowed access. Unfortunately, there is no single query that can do this with your current structure, because that would require a sort of "join" between permissions and homework. But Firestore (like all NoSQL databases), do not support joins.
You will need to model your data in such a way that is compatible with rules. You have one option that I can think of.
You could store the list users who should have read have access to a particular document in homework, within that same document, represented as a list field. The query could specify a filter based on the user's uid presence in that list field. And the rule could specify that read access only be granted to users whose IDs are present in that list.
{
id: adfsajkfsk4444,
owner: fK3ddutEpD2qQqRMXNW,
name: "Math Homework",
isDone: false,
readers: [ 'list', 'of', 'userids' ] // filter against this list field
}
The bottom line here is that you'll need to satisfy these two requirements:
Your query needs to be specific about exactly which documents that it expects to be readable. You can't use a rule to filter the results.
Your rule needs a way to determine, using nothing more complicated than the fields of the document itself, or a get() on other known documents, what the access should be for the current uid.
CONTEXT:
Suppose there are a products and orders collections on Firestore.
And an order has many products, that is (pseudo-schema):
products {
name: string
}
orders {
items: [{
product_id: (ref products)
quantity: number
}]
}
With these security rules:
match /products/{document=**} {
allow read, write: if request.auth != null;
}
match /orders/{document=**} {
allow read, write: if request.auth != null;
}
SCENARIO:
Now, suppose we have created the product A.
Then, we created the order 1 for the product A.
Next, suppose we delete the product A (which is already being used in the order 1) from the products collection.
This would let the order 1 still referencing the deleted product A.
QUESTION:
Is there a way of writing a Security Rule that prevents the deletion of products that are being used on orders?
First of all, if the goal is just consistency, you can use Functions to delete the references to A so there is no broken linkage.
However, if you specifically want to prevent deletion while linked, you'll need a different strategy, as there is no way to query another path within security rules at present.
Denormalize: keep a list of references in the product
Something like this in your data:
"{product_id}": {
"name": "...",
"orders": {
"order_id": true
}
}
Would allow you to write rules like this:
function isAuthenticated() {
return auth != null;
}
function hasLinks(resource) {
return !resource.data.links;
}
match /products/{pid} {
allow delete: if isAuthenticated() && !hasLinks(request.resource);
}
Other ideas
Using Functions: My first instinct was a queue approach (you queue the delete to the server and the serve decides if there is a link to the product and either deletes it or rejects the request). But that wouldn't work with your current structure either since you can't query across subcollections to find references to the product in each order subcollection. You'd still need a denormalized list of orders to products to use for this (making this pretty much the same as my solution above).
Archive items instead of deleting them: There's probably not a strong need to actually delete items, so archiving them instead could avoid the whole problem set.
I want to create a Cloud Firestore realtime database containing groups which users can join and share information to other members of their group. I want to preserve users anonymity so the way I see it to be implemented is:
Group creator generates a group key in format XXXX-XXXX-XXXX-XXXX
Those who want to join must have the group key which they enter in the app, after that they should be able to read, create and update data in that group
So basically the data structure is something like this:
/groups/ : [
//groups as documents
"ABCD-0000-0000-0001" : { /*group data model*/ }
"ABCD-0000-0000-0002" : { /*group data model*/ }
"ABCD-0000-0000-0003" : { /*group data model*/ }
]
The question is, what security rules should I write to permit users to read, create and update data ONLY in the group they belong to (have its group key)? At the same time, how to forbid users from finding out other groups' keys?
Your group structure can remain as is-
groups (Collection) : [
//groups as documents
"ABCD-0000-0000-0001" : { /*group data model*/ }
"ABCD-0000-0000-0002" : { /*group data model*/ }
"ABCD-0000-0000-0003" : { /*group data model*/ } ]
And to maintain the access, you can have another separate collection named group_users as-
group_users(Collection)/ <group_id>(Document)/ members(Collection)/ :
uid_1 (document)
uid_2 (document)
...
Now the rule to allow can be like-
service cloud.firestore {
match /databases/{database}/documents {
match /groups/{group_id} {
allow read, create, update: if exists(/databases/$(database)/documents/group_users/$(group_id)/members/$(request.auth.uid));
}
}
}
When a member joins the group, you will have to add the user's uid to that new collection-
group_users/<group_id>/members
For admins, you can have a similar collection, and uid will be added when the admin creates the group-
group_users/<group_id>/admins
Instead of having a separate collection outside of groups collection, you could have a collection within group itself as an alternative solition, then you will have to modify the group data model to some more extent.
Also FYI, each exists () call in security rules is billed (probably as one read operation).
And here is a detailed documentation explaining almost every possible aspect of firestore security rules.
You can save the group ID in the user's profile. Then create a rule which only allows CRU permissions if that group ID exists.
db.collection.('users').doc({userId}.update({
ABCD-0000-0000-0001: true
})
match /groups/{groupId} {
allow read, create, update: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.$(groupId) == true;
}
I would like to use a collection to maintain relation between some documents as proposed here (but slightly modified approach). I have chosen collection approach because it seems more scalable that document properties over time.
My actual data scructure
users
user1
user2
companies
co1
co2
user_co
user1
companies (collection)
co1
co2
user2
companies (collection)
co1
But with that approach, I need to do multiple queries in order to get all available companies for a specific user as we cannot perform "IN" clause.
So I need retrieve data in 2 steps:
retrieve list of company user have access to in user_co/{user}/companies
retrieve actual companies from /companies/{id}
Why 2 steps? Because I don't want to give read access to all companies to all users and querying /companies would then trigger access errorĀ.
So I got struggle with how to retrieve a single bindable list of documents retrieved from multiple calls?
I got 2 items displayed in my component but field values does not get displayed. I certainly do something wrong in the way I retrieve /company documents.
Any help would be greatly appreciated.
MyService.ts
interface Company {
Name: string;
Owner: any;
id?: any;
time?: any;
creationTime?: any;
}
interface MyCompany {
id?: any;
Name: string;
}
#Injectable()
export class CompanyService {
companiesCollection: AngularFirestoreCollection<Company>;
myCompaniesCollection: AngularFirestoreCollection<MyCompany>;
myCompanies;
constructor(private afs: AngularFirestore, private ats: AuthService) {
this.myCompaniesCollection = this.afs.collection('user_co').doc(this.ats.currentUserId).collection('companies');
this.myCompanies = this.myCompaniesCollection.snapshotChanges().map(actions => {
return actions.map(a => {
// What is the good way to retrieve /companies data from here?
return this.afs.firestore.collection("companies").doc(a.payload.doc.id).get().then(doc => {
return { id: doc.id, ...doc.data() }
}).catch(error => {
console.log("Error reading company document:", error);
});
// Original example that return data from /user_co
//return { id: a.payload.doc.id, ...a.payload.doc.data() }
})
});
}
getData() {
return this.myCompanies;
}
}
using angularfire2 5.0.0-rc.3 with firebase 4.5.2
I've finally changed the way I store data into Firestore.
As mentionned here and in many docs related to NoSQL, denormalization is the way to go to avoid "join like" and multiple queries.
Using denormalization one can group all data that is needed to process
a query in one place. This often means that for different query flows
the same data will be accessed in different combinations. Hence we
need to duplicate data, which increases total data volume.
That way, I can simply retrieve /users/{user}/companies and get all relevant info about companies user belongs to. And there is no need to be able to access all company info (settings, users, etc) for all users anyway.
New data structure
/users/{user}
user_name
/companies/{company}
company_name
/companies/{company}
name
/admins/{user}
/users/{user}
user_name
Security rules allowing admins to invite/add users to company
match /users/{usr}/companies/{co} {
// Only visible to the actual user
allow read: if request.auth.uid == usr;
// Current user can opt-out of company
allow delete: if request.auth.uid == usr ||
// Company admin can add or drop-out a user
exists(/databases/$(db)/documents/companies/$(co)/admins/$(request.auth.uid));
// Company admin can add or drop-out a user
allow create, update: if exists(/databases/$(db)/documents/companies/$(co)/admins/$(request.auth.uid));
}
match /companies/{co} {
// Company accessible for members and admins only
allow read: if
exists(/databases/$(db)/documents/companies/$(co)/members/$(request.auth.uid)) ||
exists(/databases/$(db)/documents/companies/$(co)/admins/$(request.auth.uid));
match /admins/{usr} {
// allow company creation if it does not exists
allow create: if exists(/databases/$(db)/documents/companies/$(co)) == false
// updates allowed for admins
allow update, delete: if exists(/databases/$(db)/documents/companies/$(co)/admins/$(request.auth.uid));
}
match /users/{usr} {
// writes allowed for admins
allow write: if exists(/databases/$(db)/documents/companies/$(co)/admins/$(request.auth.uid));
}
}
Counterpart
When updating /companies/{company}/[name], I also need to retrieve all users that belongs to that company through /companies/{company}/users and then update all docs in /users/{user}/companies/{company}. This can be made within a single transaction.
Consider the following protected "room" data structure at /rooms/room1:
Data:
{
rooms: {
room1: {
content: "hello world",
authorizedUsers: {
"UidOfUserA": true,
"UidOfUserB": true
}
}
}
}
Rules:
{
"rules": {
"rooms": {
"$room": {
".read": "data.child('authorizedUsers').hasChild(auth.uid)",
".write": "data.child('authorizedUsers').hasChild(auth.uid)"
}
}
}
}
Currently, UserA and UserB can read and write data to /rooms/room1. Assume that UserA was able to allow UserB to join because UserA knew UserB's UID.
However, if UserA wants to invite someone who does not yet have an account, by generating a URL and sending it to a friend (not necessarily by email,) this design needs to be expanded on.
How can I structure my rules to allow for this?
One possible solution I can think of would be:
allow users to write to their own private sandbox at /users/$uid/ (so that they can set a value in /users/$uid/activeInviteToken)
expand on the room rules to also allow users to write to /rooms/$room/ if their /users/$uid/activeInviteToken is found as a child of /rooms/$room/inviteTokens/
The workflow would then be:
UserA adds a new key to /rooms/room1/inviteTokens/ (assume it's /rooms/room1/inviteTokens/inviteToken1)
UserA generates a link with the invite token and the room name and sends it to their friend; e.g. http://example.com/?roomId=room1&inviteToken=inviteToken1
after UserC signs up, they write their inviteToken to /users/UidOfUserC/activeInviteToken = "inviteToken1"
finally, they add /rooms/room1/authorizedUsers/UidOfUserC, giving themselves access independent of the inviteToken
While not strictly necessary, completing step 4 allows the user to:
change their activeInviteToken to something else, so that they can accept an invite to another room, without losing access to room1; and
expire old inviteTokens
However, this seems overly-complicated, especially the two-step write involving /users/$uid/activeInviteToken before adding themselves to /rooms/room1/authorizedUsers.