Skip to content
This repository was archived by the owner on Mar 2, 2021. It is now read-only.

Commit 143e312

Browse files
authored
Merge pull request #18 from internet4000/feature/firebase-rules
Add Firebase rules and docs on how to deploy
2 parents 12a7137 + b335554 commit 143e312

File tree

4 files changed

+255
-0
lines changed

4 files changed

+255
-0
lines changed

.firebaserc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"projects": {
3+
"staging": "radio4000-staging",
4+
"production": "firebase-radio4000"
5+
}
6+
}

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,8 @@ To deploy to staging, run `yarn deploy`.
1717

1818
To deploy to production, first `yarn deploy` and then `now alias` (you need access to the Internet4000 team on now.sh)
1919

20+
## How to deploy Firebase rules
21+
22+
To deploy the rules in `database.rules.json`, run `firebase deploy --only database`. It will ask you whether to deploy to `staging` or `production`. See `.firebaserc` for more.
23+
2024
Peace

database.rules.json

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
{
2+
"rules": {
3+
// disallow both read and write by default
4+
// nested rules can not change rules above
5+
".read": false,
6+
".write": false,
7+
8+
"users": {
9+
// read, nobody can (the global user list)
10+
// write, only create new user if not logged in
11+
".read": false,
12+
".write": "(auth !== null) && !data.exists()",
13+
14+
// Currently commented out to allow deletes.
15+
// Make sure the the authenticated user id exists after a write
16+
// ".validate": "newData.hasChild(auth.uid)",
17+
18+
"$userID": {
19+
// read: only auth user can read themselves
20+
// write: only authed user can write a single new user, his uid, that's all
21+
// write: only user-owner can edit himself
22+
// write: can not delete (newData value can't be null)
23+
// note: first the "user" is authenticated against Firebase,
24+
// then we independently create a user model in our Firebase DB
25+
// the authentication and our USER concept are two different things
26+
".read": "$userID === auth.uid",
27+
28+
// You can write yourself only. But not delete.
29+
//".write": "($userID === auth.uid) && (newData.val() != null)",
30+
".write": "$userID === auth.uid",
31+
".validate": "newData.hasChildren(['created'])",
32+
33+
"created": {
34+
// ensure that you can not change an existing value and that is it not in the future
35+
".validate": "data.exists() && data.val() === newData.val() || newData.isNumber() && newData.val() <= now && newData.val() > now - 1000"
36+
},
37+
"settings": {
38+
".validate": "!newData.exists() || newData.isString() && root.child('userSettings').child(newData.val()).child('user').val() === $userID"
39+
},
40+
"channels": {
41+
// trick to limit to one radio
42+
".validate": "!data.exists()",
43+
"$channelID": {
44+
// we would like to prevent a user to had a channel he does no own
45+
// but we don't store on the channel@model the user who owns it
46+
// (because, historically, firebase stores the userId as provider:provider-id,
47+
// so it is extremely easy to find the user on social networks)
48+
// so instead we just prevent a user to add to himself a radio that already
49+
// has tracks in -> so no radio hijack
50+
".validate": "root.child('channels').child($channelID).exists() && !root.child('users').child(auth.uid).child('channels').child($channelID).exists() && !root.child('channels').child($channelID).child('tracks').exists()"
51+
}
52+
}/*,
53+
"$other": {
54+
".validate": false
55+
}*/
56+
}
57+
},
58+
59+
"userSettings": {
60+
".read": false,
61+
".write": false,
62+
63+
"$userSettingsID": {
64+
// read: only auth user can read its settings
65+
// write: only user-owner can edit his settings
66+
// write: only logged-in-user without a user settings can create a new one to himself
67+
".read": "auth != null && (data.child('user').val() === auth.uid)",
68+
".write": "auth != null && ((data.child('user').val() === auth.uid) || (root.child('users').child(auth.uid).hasChild('settings') === false))",
69+
"user": {
70+
".validate": "newData.val() === auth.uid"
71+
},
72+
"isRemoteActive": {
73+
".validate": "newData.isBoolean()"
74+
},
75+
"$other": {
76+
".validate": false
77+
}
78+
}
79+
},
80+
81+
"channels": {
82+
".read": true,
83+
".write": false,
84+
".indexOn": ["slug", "isFeatured"],
85+
86+
"$channelID": {
87+
".read": true,
88+
89+
// write: only the user owner can write a channel
90+
// write: only a user with no channel can write a new one to himself
91+
".write": "auth != null && (root.child('users').child(auth.uid).child('channels').child($channelID).exists() || (!data.exists() && !root.child('users').child(auth.uid).child('channels').exists()))",
92+
".validate": "newData.hasChildren(['slug', 'title', 'created']) || !newData.exists()",
93+
94+
"body": {
95+
".validate": "newData.isString() && newData.val().length < 300"
96+
},
97+
"channelPublic": {
98+
".validate": "newData.isString() && root.child('channelPublics').child(newData.val()).child('channel').val() == $channelID"
99+
},
100+
"created": {
101+
// Ensure type::number and that you can not update it
102+
".validate": "data.exists() && data.val() === newData.val() || newData.isNumber() && newData.val() <= now && newData.val() > now - 1000"
103+
},
104+
"updated": {
105+
// Ensure type::number and that you can not update it
106+
".validate": "data.exists() && data.val() === newData.val() || newData.isNumber() && newData.val() <= now && newData.val() > now - 1000"
107+
},
108+
"isFeatured": {
109+
".validate": "newData.isBoolean() && (data.exists() && newData.val() === data.val() || newData.val() === false)"
110+
},
111+
"link": {
112+
".validate": "newData.isString() && newData.val().length > 2 && newData.val().length < 100"
113+
},
114+
// todo: create a separate table to maintain unique slugs otherwise slug can be hijacked
115+
"slug": {
116+
".validate": "newData.isString() && newData.val().length > 2 && newData.val().length < 100"
117+
},
118+
"title": {
119+
".validate": "newData.isString() && newData.val().length > 2 && newData.val().length < 32"
120+
},
121+
"tracks": {
122+
"$track": {
123+
".validate": "root.child('tracks').hasChild($track) && root.child('tracks').child($track).child('channel').val() === $channelID"
124+
}
125+
},
126+
"favoriteChannels": {
127+
"$favoriteChannel": {
128+
".validate": "root.child('channels').child($favoriteChannel).exists() && !root.child('users').child(auth.uid).child('channels').child($favoriteChannel).exists()"
129+
}
130+
},
131+
"images": {
132+
"$image": {
133+
".validate": "root.child('images').hasChild($image) && root.child('images').child($image).child('channel').val() === $channelID"
134+
}
135+
},
136+
"$other": {
137+
".validate": false
138+
}
139+
}
140+
},
141+
142+
"channelPublics": {
143+
// read: no one can read the full list of the channelPublics
144+
// write: no one can write the list
145+
".read": false,
146+
".write": false,
147+
".indexOn": ["followers"],
148+
"$channelPublic": {
149+
".read": true,
150+
// write: user is logged in && has one channel
151+
".write": "auth !== null && root.child('users').child(auth.uid).child('channels').exists()",
152+
".validate": "newData.hasChild('channel')",
153+
154+
// there is no existing data
155+
// && user has the channel newData in is channels
156+
// && this channel has no publicChannel
157+
"channel": {
158+
".validate": "data.exists() && data.val() === newData.val() || !data.exists() && root.child('users').child(auth.uid).child('channels').child(newData.val()).exists() && !root.child('channels').child(newData.val()).child('channelPublic').exists()"
159+
},
160+
"followers": {
161+
// validate: user can only add a radio he owns
162+
// validate: owner can't add himself
163+
"$follower": {
164+
".validate": "root.child('users').child(auth.uid).child('channels').hasChild($follower) && root.child('channels').child($follower).child('channelPublic').val() !== $channelPublic"
165+
}
166+
},
167+
"$other": {
168+
".validate": false
169+
}
170+
}
171+
},
172+
173+
"images": {
174+
".read": true,
175+
".write": false,
176+
"$imageID": {
177+
// ".write": "auth !== null && root.child('users').child(auth.uid).child('channels').child(newData.child('channel').val()).exists()",
178+
".write": "auth !== null && newData.exists() && root.child('users/' + auth.uid + '/channels').child(newData.child('channel').val()).exists() || auth !== null && !newData.exists() && root.child('users/' + auth.uid + '/channels').child(data.child('channel').val()).exists()",
179+
".validate": "newData.hasChildren(['channel', 'src'])",
180+
"channel": {
181+
".validate": "root.child('users').child(auth.uid).child('channels').child(newData.val()).exists()"
182+
},
183+
"src": {
184+
".validate": "newData.isString()"
185+
},
186+
"created": {
187+
".validate": "data.exists() && data.val() === newData.val() || newData.isNumber() && newData.val() <= now && newData.val() > now - 1000"
188+
},
189+
"$other": {
190+
".validate": false
191+
}
192+
}
193+
},
194+
195+
"tracks": {
196+
".read": true,
197+
".write": false,
198+
".indexOn": ["channel", "title"],
199+
200+
"$trackID": {
201+
// ".write": "auth !== null",
202+
// ".write": "auth !== null && (!newData.exists && data.child('channel').val())",
203+
// ".write": "auth !== null && root.child('users/' + auth.uid + '/channels').child('-Ko1h4nyODlOuwfIyQRh').exists()",
204+
// ".write": "auth !== null && root.child('users/' + auth.uid + '/channels').child(newData.child('channel').val()).exists()",
205+
".write": "auth !== null && newData.exists() && root.child('users/' + auth.uid + '/channels').child(newData.child('channel').val()).exists() || auth !== null && !newData.exists() && root.child('users/' + auth.uid + '/channels').child(data.child('channel').val()).exists()",
206+
// ".write": "auth !== null && (root.child('users').child(auth.uid).child('channels').child(data.child('channel').val()).exists() || root.child('users').child(auth.uid).child('channels').child(newData.child('channel').val()).exists())",
207+
".validate": "newData.hasChildren(['channel', 'url', 'ytid', 'title', 'created'])",
208+
209+
"created": {
210+
// ".validate": "data.exists() && data.val() === newData.val() || newData.isNumber() && newData.val() <= now && newData.val() > now - 1000"
211+
".validate": "data.exists() && data.val() === newData.val() || newData.isNumber() && newData.val() <= now"
212+
},
213+
"url": {
214+
".validate": "newData.isString() && newData.val().length > 3"
215+
},
216+
"title": {
217+
".validate": "newData.isString() && newData.val().length > 0"
218+
},
219+
"body": {
220+
".validate": "newData.isString()"
221+
},
222+
"ytid": {
223+
".validate": "newData.isString()"
224+
},
225+
"channel": {
226+
// can only be updated if the value matches the authenticated users channel id
227+
".validate": "newData.isString() && root.child('users').child(auth.uid).child('channels').child(newData.val()).exists()"
228+
},
229+
"$other": {
230+
".validate": false
231+
}
232+
}
233+
},
234+
235+
"$others": {
236+
".validate": false
237+
}
238+
}
239+
}
240+

firebase.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"database": {
3+
"rules": "database.rules.json"
4+
}
5+
}

0 commit comments

Comments
 (0)