-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstorage.rules
More file actions
156 lines (140 loc) · 7.54 KB
/
Copy pathstorage.rules
File metadata and controls
156 lines (140 loc) · 7.54 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
156
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
function isCmsUser() {
return request.auth != null &&
firestore.exists(/databases/(default)/documents/authors/$(request.auth.uid)) &&
firestore.get(/databases/(default)/documents/authors/$(request.auth.uid)).data.role in ['author', 'admin', 'super'];
}
function isAdminUser() {
return request.auth != null &&
firestore.exists(/databases/(default)/documents/authors/$(request.auth.uid)) &&
firestore.get(/databases/(default)/documents/authors/$(request.auth.uid)).data.role in ['admin', 'super'];
}
function canModerate() {
return request.auth != null &&
firestore.exists(/databases/(default)/documents/authors/$(request.auth.uid)) &&
firestore.get(/databases/(default)/documents/authors/$(request.auth.uid)).data.role in ['moderator', 'admin', 'super'];
}
function isStaffUser() {
return request.auth != null &&
firestore.exists(/databases/(default)/documents/authors/$(request.auth.uid)) &&
firestore.get(/databases/(default)/documents/authors/$(request.auth.uid)).data.role in ['author', 'moderator', 'admin', 'super'];
}
function isActiveReaderAccount() {
return request.auth != null &&
firestore.exists(/databases/(default)/documents/users/$(request.auth.uid)) &&
(!firestore.get(/databases/(default)/documents/users/$(request.auth.uid)).data.keys().hasAny(['status']) ||
firestore.get(/databases/(default)/documents/users/$(request.auth.uid)).data.status in ['active', 'warning'] ||
(firestore.get(/databases/(default)/documents/users/$(request.auth.uid)).data.status == 'suspended' &&
firestore.get(/databases/(default)/documents/users/$(request.auth.uid)).data.suspendedUntil <= request.time)) &&
!firestore.get(/databases/(default)/documents/users/$(request.auth.uid)).data.keys().hasAny(['bannedAt']) &&
(!firestore.get(/databases/(default)/documents/users/$(request.auth.uid)).data.keys().hasAny(['suspendedUntil']) ||
firestore.get(/databases/(default)/documents/users/$(request.auth.uid)).data.suspendedUntil <= request.time);
}
// A newly authenticated reader may choose an avatar before the users doc
// is created. Banned accounts keep their users doc, so this cannot bypass
// active-account enforcement for an existing reader.
function canCreateOwnReaderProfile() {
return request.auth != null &&
!firestore.exists(/databases/(default)/documents/users/$(request.auth.uid));
}
function canManageArticle(articleId) {
return isAdminUser() ||
(isCmsUser() &&
firestore.exists(/databases/(default)/documents/articles/$(articleId)) &&
firestore.get(/databases/(default)/documents/articles/$(articleId)).data.authorUID == request.auth.uid);
}
function isPublishedArticle(articleId) {
return firestore.exists(/databases/(default)/documents/articles/$(articleId)) &&
firestore.get(/databases/(default)/documents/articles/$(articleId)).data.publish == true;
}
match /users/{userId}/profile/{fileName} {
allow get: if fileName == 'avatar';
allow create, update: if request.auth != null &&
request.auth.uid == userId &&
(isActiveReaderAccount() || canCreateOwnReaderProfile()) &&
fileName == 'avatar' &&
request.resource.size <= 5 * 1024 * 1024 &&
request.resource.contentType.matches('image/(jpeg|png|webp)');
allow delete: if (request.auth != null && request.auth.uid == userId) || isAdminUser();
}
// Every team member may update only the avatar named after their own UID.
// This lets moderators manage their profile without granting asset access.
match /avatars/team/{fileName} {
allow get: if true;
allow create, update: if request.auth != null &&
firestore.exists(/databases/(default)/documents/authors/$(request.auth.uid)) &&
(fileName == request.auth.uid + '.webp' ||
firestore.get(/databases/(default)/documents/authors/$(request.auth.uid)).data.role in ['admin', 'super']) &&
fileName.matches('[a-zA-Z0-9_-]{1,128}\\.webp') &&
request.resource.size <= 5 * 1024 * 1024 &&
request.resource.contentType.matches('image/(jpeg|png|webp)');
allow delete: if (
request.auth != null &&
firestore.exists(/databases/(default)/documents/authors/$(request.auth.uid)) &&
fileName == request.auth.uid + '.webp'
) || isAdminUser();
}
// Legacy flat attachment paths remain readable/deletable but cannot receive
// new objects. New uploads require a short-lived server reservation.
match /comments/{userId}/{fileName} {
allow get: if true;
allow create, update: if false;
allow delete: if (request.auth != null && request.auth.uid == userId) || canModerate();
}
match /comments/{userId}/{reservationId}/{fileName} {
function reservation() {
return firestore.get(/databases/(default)/documents/commentUploadReservations/$(reservationId)).data;
}
allow get: if true;
// Storage rules may read at most two Firestore documents. Account checks
// stay on reserveCommentUploads / createComment so this rule only reads
// the reservation.
allow create: if request.auth != null &&
request.auth.uid == userId &&
reservationId.matches('[a-zA-Z0-9_-]{20,64}') &&
fileName.matches('[0-3]\\.(webp|jpg)') &&
reservation().uid == userId &&
reservation().expiresAt >= request.time &&
fileName in reservation().fileNames &&
request.resource.size <= 2 * 1024 * 1024 &&
request.resource.contentType.matches('image/(webp|jpeg)');
// Attachment objects are content-addressed by a random name. Edits must
// create a new object so one comment cannot silently replace another's image.
allow update: if false;
allow delete: if (request.auth != null && request.auth.uid == userId) || canModerate();
}
match /authors/{userId}/{fileName} {
allow get: if true;
allow create, update: if isAdminUser() &&
fileName.matches('[a-zA-Z0-9_-]{1,128}\\.(webp|jpg)') &&
request.resource.size <= 2 * 1024 * 1024 &&
request.resource.contentType.matches('image/(webp|jpeg)');
allow delete: if isAdminUser();
}
// Authors can manage article-scoped assets; other bucket areas are
// reserved for admins. Public pages may fetch objects but cannot enumerate.
match /Articles/{articleId}/{allPaths=**} {
allow get: if isPublishedArticle(articleId) || canManageArticle(articleId);
allow list: if canManageArticle(articleId);
allow create, update: if canManageArticle(articleId) &&
request.resource.size <= 25 * 1024 * 1024;
allow delete: if canManageArticle(articleId);
}
// Capability-signed download URLs are generated server-side for these ZIPs.
// Do not grant anonymous reads: the signed URL remains the only public access path.
match /temp_downloads/{fileName} {
allow read, write: if isAdminUser();
}
// Unknown/legacy namespaces are private. Administrators retain access for
// migration and cleanup, but a narrow rule above is required for public content.
match /{topLevel}/{allPaths=**} {
allow read: if isAdminUser();
// Comment objects stay immutable for every client role. Administrators
// can still remove them through the explicit moderation rule above.
allow create, update, delete: if isAdminUser() &&
topLevel != 'comments';
}
}
}