Solution 1: One User Document per User
The most simple solution is to have 1 User
Document to store all user data.
name: "Desmond"
email: "[email protected]"
roles: ["user", "admin"]
address: "19, Almond Street, ..."
stat:
lastLoginDate: "2019-02-28T15:53:49.147"
sessionCount: 3
active: true
NOTE: We store UserId
as Document ID.
There is a few obvious drawbacks with this solution:
email
is exposed, since Firestore don't allow partial get/fetch or limit fields.- with
roles
shown to anyone (since everyone should be able to list all users), admin account could be identified and targetted. - the document will get bloated (bandwidth concern) and privacy issues, due to private content like
stat
andaddress
is exposed.
Security Rules for User
document.
- Allow anyone to perform read/get/list. Must signin to perform create/update.
- Allow sigin user to create
User
document, where the Document ID ({userId}
) must match UserID (request.auth.uid
). - User can only create/update
User
withroles = ['user']
, while Admin can do anything. - Allow owner to create/update with
roles
data restriction (we don't want user to assign admin priviledge to themselves). Other users cannot update documents where they are not the owner. - Only allow Admin to delete
User
document.
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if false;
}
match /users/{userId} {
function isAdmin() {
return isLogin() &&
(request.auth.uid == 'QYHqCKCGSbY3qqMSliqNhR3j9QC2' ||
'admin' in get(/databases/$(database)/documents/users/$(request.auth.uid)).data.roles)
}
function isOwner() {
return userId == request.auth.uid;
}
function isLogin() {
return request.auth != null
}
function validData() {
return 'roles' in request.resource.data && request.resource.data.roles == ['user']
}
// anyone
allow read: if true;
// allow get: if true;
// allow list: if true;
// allow create: if (isLogin() && validUserCreate()) || isAdmin();
allow create: if (isOwner() && validData()) || isAdmin();
allow update: if (isOwner() && validData()) || isAdmin();
allow delete: if isAdmin();
}
}
}
NOTE: Obserse validData
. Even if we perform partial updates without the field roles
, request.resource.data.roles
will still be populated if existing data contain the field roles
.
Solution 2: Public User and Private User Document
Public User: store in users
collection.
created: "2019-02-28T15:53:49.147"
name: "Desmond"
bio: "I like ..."
profilePictureUrl: "https://..."
active: true
Private User: store in userPrivates
collection.
email: "[email protected]"
roles: ["user", "admin"]
address: "19, Almond Street, ..."
stat:
lastLoginDate: "2019-02-28T15:53:49.147"
sessionCount: 3
active: true
NOTE: Both public and private User
use UserId
as Document ID.
NOTE: Notice active
is duplicated on both collections. This depends on your use case, as we might want to duplicate some fields to avoid too much get/fetch (since Firestore pricing depends on number of operations).
You might be thinking of storing userPrivates
as subcollection of users
(e.g. users/{userId}/privates/{userId}
), but doing so would make the following query impossible:
- Get all users with roles of admin.
- Get all users with sessionCount more than 100.
The drawbacks with users
(Public User) is that it might be slightly bloated.
- If we query
comments
withusers
, we just needname
andprofilePictureUrl
, probably don't needcreated
orbio
. - If we view the user's profile page, then we need the full
users
data.