-
-
Notifications
You must be signed in to change notification settings - Fork 20
Expand file tree
/
Copy pathfirestore.rules
More file actions
161 lines (148 loc) · 8.63 KB
/
Copy pathfirestore.rules
File metadata and controls
161 lines (148 loc) · 8.63 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
157
158
159
160
161
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Security Functions
function isSignedIn() {
return request.auth != null;
}
function isSignedInWithUser(userId) {
return request.auth.uid == userId;
}
function hasRole(role) {
return request.auth.token.role == role;
}
function hasAnyRole(roles) {
return request.auth.token.role in roles;
}
function hasPermission(permission) {
return hasRole('custom') && permission in request.auth.token.permissions;
}
function hasAnyPermission(permissions) {
return hasRole('custom') && request.auth.token.permissions.hasAny(permissions);
}
// Spaces
match /spaces/{spaceId} {
allow read: if isSignedIn() && hasAnyRole(['custom','admin']);
allow write: if isSignedIn() && (hasRole('admin') || hasPermission('SPACE_MANAGEMENT'));
// Translations
match /translations/{translationId} {
allow read: if isSignedIn() && (hasRole('admin') || hasPermission('TRANSLATION_READ'));
allow create: if isSignedIn() && (hasRole('admin') || hasPermission('TRANSLATION_CREATE'));
allow update: if isSignedIn() && (hasRole('admin') || hasPermission('TRANSLATION_UPDATE'));
allow delete: if isSignedIn() && (hasRole('admin') || hasPermission('TRANSLATION_DELETE'));
}
// Schemas
match /schemas/{schemaId} {
allow read: if isSignedIn() && (hasRole('admin') || hasAnyPermission(['SCHEMA_READ','CONTENT_READ']));
allow create: if isSignedIn() && (hasRole('admin') || hasPermission('SCHEMA_CREATE'));
allow update: if isSignedIn() && (hasRole('admin') || hasPermission('SCHEMA_UPDATE'));
allow delete: if isSignedIn() && (hasRole('admin') || hasPermission('SCHEMA_DELETE'));
}
// Content
match /contents/{contentId} {
allow read: if isSignedIn() && (hasRole('admin') || hasPermission('CONTENT_READ'));
allow create: if isSignedIn() && (hasRole('admin') || hasPermission('CONTENT_CREATE'));
allow update: if isSignedIn() && (hasRole('admin') || hasPermission('CONTENT_UPDATE'));
allow delete: if isSignedIn() && (hasRole('admin') || hasPermission('CONTENT_DELETE'));
}
// Assets
match /assets/{assetId} {
allow read: if isSignedIn() && (hasRole('admin') || hasAnyPermission(['ASSET_READ','CONTENT_READ']));
allow create: if isSignedIn() && (hasRole('admin') || hasPermission('ASSET_CREATE'));
allow update: if isSignedIn() && (hasRole('admin') || hasPermission('ASSET_UPDATE'));
allow delete: if isSignedIn() && (hasRole('admin') || hasPermission('ASSET_DELETE'));
}
// Tasks
// The task `kind` doubles as the permission that may create it (e.g. CONTENT_IMPORT), so a
// holder of one import/export permission cannot make the task trigger - which runs with
// Admin SDK rights - import or export a different kind of data. ASSET_REGEN_METADATA is
// not a permission, so only admins can create it.
function canManageTaskKind(kind) {
return hasRole('admin') || (kind != 'ASSET_REGEN_METADATA' && hasPermission(kind));
}
// Import tasks name the staged upload the trigger moves into the task folder. Pin it to this
// space's staging folder so a task cannot point the trigger at any other bucket object.
function isValidTmpPath(spaceId, data) {
return data.tmpPath is string && data.tmpPath.matches('spaces/' + spaceId + '/tasks/tmp/[0-9]+');
}
function isValidNewTask(spaceId, data) {
return data.status == 'INITIATED'
&& (
(data.kind in ['ASSET_EXPORT','CONTENT_EXPORT'] && data.keys().hasOnly(['kind','status','path','createdAt','updatedAt']))
|| (data.kind == 'SCHEMA_EXPORT' && data.keys().hasOnly(['kind','status','createdAt','updatedAt']))
|| (data.kind == 'TRANSLATION_EXPORT' && data.keys().hasOnly(['kind','status','locale','createdAt','updatedAt']))
|| (data.kind == 'ASSET_REGEN_METADATA' && data.keys().hasOnly(['kind','status','createdAt','updatedAt']))
|| (data.kind in ['ASSET_IMPORT','CONTENT_IMPORT','SCHEMA_IMPORT'] && data.keys().hasOnly(['kind','status','tmpPath','file','createdAt','updatedAt']) && isValidTmpPath(spaceId, data))
|| (data.kind == 'TRANSLATION_IMPORT' && data.keys().hasOnly(['kind','status','type','locale','tmpPath','file','createdAt','updatedAt']) && isValidTmpPath(spaceId, data))
);
}
match /tasks/{taskId} {
allow read: if isSignedIn() && (hasRole('admin') || hasAnyPermission(['ASSET_IMPORT','ASSET_EXPORT','CONTENT_IMPORT','CONTENT_EXPORT','SCHEMA_IMPORT','SCHEMA_EXPORT','TRANSLATION_IMPORT','TRANSLATION_EXPORT']));
allow create: if isSignedIn() && canManageTaskKind(request.resource.data.kind) && isValidNewTask(spaceId, request.resource.data);
// Status, results and errors are written only by the task trigger (Admin SDK).
allow update: if false;
allow delete: if isSignedIn() && canManageTaskKind(resource.data.kind);
// Task Logs
match /logs/{logId} {
allow read: if isSignedIn() && (hasRole('admin') || hasAnyPermission(['ASSET_IMPORT','ASSET_EXPORT','CONTENT_IMPORT','CONTENT_EXPORT','SCHEMA_IMPORT','SCHEMA_EXPORT','TRANSLATION_IMPORT','TRANSLATION_EXPORT']));
}
}
// Tokens
match /tokens/{tokenId} {
allow read: if isSignedIn() && (hasRole('admin') || hasPermission('SPACE_MANAGEMENT'));
allow write: if isSignedIn() && (hasRole('admin') || hasPermission('SPACE_MANAGEMENT'));
}
// WebHooks
// First line of defence only: delivery (functions/src/utils/webhook-request.ts) re-checks the
// URL and refuses private addresses at connect time. http is allowed for local targets so the
// emulator can be used against a local receiver; production delivery refuses it.
function isValidWebhookUrl(url) {
return url is string && url.size() <= 2048
&& (url.matches('https://[^/?#]+([/?#].*)?') || url.matches('http://(localhost|127[.]0[.]0[.]1)(:[0-9]+)?([/?#].*)?'));
}
match /webhooks/{webhookId} {
allow read: if isSignedIn() && (hasRole('admin') || hasPermission('SPACE_MANAGEMENT'));
allow create: if isSignedIn() && (hasRole('admin') || hasPermission('SPACE_MANAGEMENT')) && isValidWebhookUrl(request.resource.data.url);
allow update: if isSignedIn() && (hasRole('admin') || hasPermission('SPACE_MANAGEMENT')) && isValidWebhookUrl(request.resource.data.url);
allow delete: if isSignedIn() && (hasRole('admin') || hasPermission('SPACE_MANAGEMENT'));
// WebHook Logs
match /logs/{logId} {
allow read: if isSignedIn() && (hasRole('admin') || hasPermission('SPACE_MANAGEMENT'));
}
}
}
// Users
// `role`, `permissions` and `lock` are copied into the user's custom claims by the user.onUpdate
// trigger, so writing this document is granting access. Admins may write any user. A custom user
// with USER_MANAGEMENT may only manage users they fully outrank: never themselves, never an
// admin, never someone holding a permission they lack - and may only grant permissions they
// hold themselves, never the admin role.
function holdsAll(permissions) {
return permissions.toSet().difference(request.auth.token.permissions.toSet()).size() == 0;
}
function canManageExistingUser(userId) {
return hasPermission('USER_MANAGEMENT')
&& !isSignedInWithUser(userId)
&& resource.data.get('role', null) != 'admin'
&& holdsAll(resource.data.get('permissions', []));
}
match /users/{userId} {
allow read: if isSignedIn() && ( isSignedInWithUser(userId) || (hasRole('admin') || hasPermission('USER_MANAGEMENT')));
// Profiles are created by the Auth trigger and user.sync (Admin SDK), never by the browser.
allow create: if isSignedIn() && hasRole('admin');
allow update: if isSignedIn() && (
hasRole('admin')
|| (canManageExistingUser(userId)
&& request.resource.data.diff(resource.data).affectedKeys().hasOnly(['role','permissions','lock','updatedAt'])
&& request.resource.data.get('role', null) in [null, 'custom']
&& holdsAll(request.resource.data.get('permissions', [])))
);
allow delete: if isSignedIn() && (hasRole('admin') || canManageExistingUser(userId));
}
// Configs
match /configs/{configId} {
allow read: if isSignedIn() && (hasRole('admin') || hasPermission('SETTINGS_MANAGEMENT'));
allow write: if isSignedIn() && (hasRole('admin') || hasPermission('SETTINGS_MANAGEMENT'));
}
}
}