-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfirestore.rules
More file actions
155 lines (143 loc) · 6.53 KB
/
firestore.rules
File metadata and controls
155 lines (143 loc) · 6.53 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
/**
* Core Philosophy:
* This ruleset implements a strict user-ownership model, often referred to as a
* "data silo" pattern. Every piece of data, including user profiles, tasks,
* and labels, is private to the user who created it. There are no shared or
* public resources; a user can only ever access documents that exist within
* their own data tree.
*
* Data Structure:
* All application data is hierarchically organized under the /users/{userId}
* collection. This top-level collection holds user profile documents, and each
* user document serves as the root for their private subcollections, such as
* /tasks and /labels. This structure makes ownership explicit in the path.
*
* Key Security Decisions:
* - Strict Ownership: All rules are based on the principle that the authenticated
* user's UID must match the {userId} in the document path.
* - No Public Access: There are no publicly readable collections. All reads and
* writes require user authentication.
* - No User Listing: It is not possible to list or query the top-level /users
* collection, preventing enumeration of all application users.
* - Relational Integrity: On document creation, rules enforce that internal
* ownership fields (like `userId` on a Task) match the user ID in the path.
* On update, these ownership fields are made immutable to prevent re-association.
*
* Denormalization for Authorization:
* This ruleset relies on "path-based authorization." The user's UID in the
* document path is the primary piece of information needed for security decisions,
* eliminating the need for slow and costly `get()` calls to other documents.
* Additionally, Task and Label documents contain a denormalized `userId` field
* to ensure data consistency and prevent documents from being moved between users.
*
* Structural Segregation:
* The data model is entirely built on structural segregation. All data related
* to a single user is contained under `/users/{userId}`. This creates a secure
* boundary, ensuring that queries and direct gets are naturally scoped and
* cannot leak data from other users.
*/
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// -------------------------------------------------------------------------
// Helper Functions
// -------------------------------------------------------------------------
/**
* Returns true if the user is signed in.
*/
function isSignedIn() {
return request.auth != null;
}
/**
* Returns true if the requesting user's UID matches the given userId.
* This is the primary function for checking document ownership.
*/
function isOwner(userId) {
return isSignedIn() && request.auth.uid == userId;
}
/**
* Returns true if the user is the owner and the document already exists.
* Used for safe updates and deletes to prevent acting on non-existent data.
*/
function isExistingOwner(userId) {
return isOwner(userId) && resource != null;
}
/**
* Validates that the user document's internal `id` field matches the
* document's ID in the path upon creation.
*/
function isSelfIdentified(userId) {
return request.resource.data.id == userId;
}
/**
* Validates that the internal `userId` field of a subcollection document
* (e.g., Task, Label) matches the owner's ID from the path.
*/
function isOwnedByUser(userId) {
return request.resource.data.userId == userId;
}
/**
* Enforces immutability of the user's `id` field after creation.
*/
function isIdImmutable() {
return request.resource.data.id == resource.data.id;
}
/**
* Enforces immutability of the subcollection document's `userId` field.
*/
function isUserIdImmutable() {
return request.resource.data.userId == resource.data.userId;
}
// -------------------------------------------------------------------------
// Collection Rules
// -------------------------------------------------------------------------
/**
* @description Manages user profile documents.
* @path /users/{userId}
* @allow (create) An authenticated user creating their own profile document: auth.uid === userId.
* @allow (get, update, delete) The owner of the profile document.
* @deny (list) Any user attempting to list all user profiles.
* @deny (get) A user trying to read another user's profile: auth.uid !== userId.
* @principle Restricts access to a user's own data tree and prevents user enumeration.
*/
match /users/{userId} {
allow get: if isOwner(userId);
allow list: if false;
allow create: if isOwner(userId) && isSelfIdentified(userId);
allow update: if isExistingOwner(userId) && isIdImmutable();
allow delete: if isExistingOwner(userId);
}
/**
* @description Manages tasks, which are private to each user.
* @path /users/{userId}/tasks/{taskId}
* @allow (create) An authenticated user creating a task in their own subcollection.
* @allow (get, list, update, delete) The user who owns the tasks.
* @deny (create) A user trying to create a task under another user's profile: auth.uid !== userId.
* @deny (get, list) A user trying to read tasks belonging to another user.
* @principle Enforces document ownership within a user-specific subcollection.
*/
match /users/{userId}/tasks/{taskId} {
allow get: if isOwner(userId);
allow list: if isOwner(userId);
allow create: if isOwner(userId) && isOwnedByUser(userId);
allow update: if isExistingOwner(userId) && isUserIdImmutable();
allow delete: if isExistingOwner(userId);
}
/**
* @description Manages labels, which are private to each user for organizing tasks.
* @path /users/{userId}/labels/{labelId}
* @allow (create) An authenticated user creating a label in their own subcollection.
* @allow (get, list, update, delete) The user who owns the labels.
* @deny (update) A user trying to modify a label belonging to another user: auth.uid !== userId.
* @deny (get, list) A user trying to read labels belonging to another user.
* @principle Enforces document ownership within a user-specific subcollection.
*/
match /users/{userId}/labels/{labelId} {
allow get: if isOwner(userId);
allow list: if isOwner(userId);
allow create: if isOwner(userId) && isOwnedByUser(userId);
allow update: if isExistingOwner(userId) && isUserIdImmutable();
allow delete: if isExistingOwner(userId);
}
}
}