-
-
Notifications
You must be signed in to change notification settings - Fork 13
/
Copy pathAccessControl.ino
310 lines (255 loc) · 12 KB
/
AccessControl.ino
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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
/**
* ABOUT:
*
* The example to show how to use custom claims and UID with security rules to control Firebase Realtime database access.
*
* We use the sync functions in this example because it is easy to describe the processes stype by step.
*
* For more details of using claims with security rules to control Firebase services accesses,
* visit https://firebase.google.com/docs/auth/admin/custom-claims
*
* For security rules information.
* https://firebase.google.com/docs/rules
* https://firebase.google.com/docs/rules/rules-language
* https://firebase.google.com/docs/database/security/rules-conditions
*
* This example uses the CustomAuth class for authentication for setting our custom UID and additional claims.
*
* The LegacyToken class (database secret) was used only for security rules read and modification process.
*
* The FirebaseJson library used in this example is available to install in IDE's Library Manager or
* can be downloaded from https://github.com/mobizt/FirebaseJson.
*
* The syntaxes used in this example are described in example/App/AppInitialization/Sync/CustomAuth/CustomAuth.ino
*
* The complete usage guidelines, please read README.md or visit https://github.com/mobizt/FirebaseClient
*
*/
#include <Arduino.h>
#include <FirebaseClient.h>
#include "ExampleFunctions.h" // Provides the functions used in the examples.
#include <FirebaseJson.h>
#define WIFI_SSID "WIFI_AP"
#define WIFI_PASSWORD "WIFI_PASSWORD"
#define API_KEY "Web_API_KEY"
#define FIREBASE_PROJECT_ID "PROJECT_ID"
#define FIREBASE_CLIENT_EMAIL "CLIENT_EMAIL"
const char PRIVATE_KEY[] PROGMEM = "-----BEGIN PRIVATE KEY-----XXXXXXXXXXXX-----END PRIVATE KEY-----\n";
#define DATABASE_SECRET "DATABASE_SECRET"
#define DATABASE_URL "URL"
void processData(AsyncResult &aResult);
bool mofifyRules(const String &controlPath, const String &readCondition, const String &writeCondition, const String &databaseSecret);
SSL_CLIENT ssl_client;
// This uses built-in core WiFi/Ethernet for network connection.
// See examples/App/NetworkInterfaces for more network examples.
using AsyncClient = AsyncClientClass;
AsyncClient aClient(ssl_client);
CustomAuth custom_auth(API_KEY, FIREBASE_CLIENT_EMAIL, FIREBASE_PROJECT_ID, PRIVATE_KEY, "peter" /* UID */, "" /* claims can be set later */, 3600 /* expire period in seconds (<3600) */);
FirebaseApp app;
RealtimeDatabase Database;
AsyncResult databaseResult;
unsigned long ms = 0;
void setup()
{
Serial.begin(115200);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("Connecting to Wi-Fi");
while (WiFi.status() != WL_CONNECTED)
{
Serial.print(".");
delay(300);
}
Serial.println();
Serial.print("Connected with IP: ");
Serial.println(WiFi.localIP());
Serial.println();
Firebase.printf("Firebase Client v%s\n", FIREBASE_CLIENT_VERSION);
set_ssl_client_insecure_and_buffer(ssl_client);
// Assign the valid time only required for authentication process with ServiceAuth and CustomAuth.
app.setTime(get_ntp_time());
// The following process will modify the Realtime Database security rules to allow the read/write accesses only at UsersData/$userId
// if the auth.token variables are match the custom claims we set.
//
// Here is the final security rules we want to set in this example.
//
// {
// "rules": {
// "examples": {
// "AccessControl": {
// "$resource": {
// "$group": {
// "$userId": {
// ".read": "($userId === auth.uid && auth.token.resource === $resource && auth.token.group === $group)",
// ".write": "($userId === auth.uid && auth.token.resource === $resource && auth.token.group === $group)"
// }
// }
// }
// }
// }
// }
// }
//
// We use $ variable in the rules e.g. $resource, $group, and $userId to capture the path segment that are used to compare with the auth variables
// that we set e.g. $userId will be compared with UID (auth.uid), $resource will be compared with resource claim (auth.token.resource),
// and $group will be compared with group claim (auth.token.group).
//
// For more information, visit https://firebase.google.com/docs/database/security/rules-conditions#using_variables_to_capture_path_segments
//
// =========
// IMPORTANT
// =========
// To allow read/write access only the conditions we set above,
// you have to remove the following rules from your security rules (if it exists).
// ".read": "auth != null"
// ".write": "auth != null"
// ".read": "true"
// ".write": "true"
// ".read": "some other conditions that allow access by date"
// ".write": "some other conditions that allow access by date"
String controlPath = "/examples/AccessControl/$resource/$group/$userId";
String readConditions = "($userId === auth.uid && auth.token.resource === $resource && auth.token.group === $group)";
String writeConditions = readConditions;
mofifyRules(controlPath, readConditions, writeConditions, DATABASE_SECRET);
// We will set the claims to the token we used here (ID token using CustomAuth).
// We set the values of the claims to math the auth.token varibles values in the security rules conditions.
// The claims string must be JSON serialized string when passing to CustomAuth::setClaims or CustomAuth class constructor.
// For more details about custom claims, please see https://firebase.google.com/docs/auth/admin/custom-claims.
// The resource claim value can be access via auth.token.resource in the security rules.
// And group claim value can be access via auth.token.group.
String claims = "{\"resource\":\"products\", \"group\":\"user\"}";
custom_auth.setClaims(claims);
Serial.println("Initializing app...");
initializeApp(aClient, app, getAuth(custom_auth), auth_debug_print, "🔐 authTask");
// Or intialize the app and wait.
// initializeApp(aClient, app, getAuth(custom_auth), 120 * 1000, auth_debug_print);
app.getApp<RealtimeDatabase>(Database);
Database.url(DATABASE_URL);
}
void loop()
{
// To maintain the authentication and async tasks
app.loop();
if (app.ready() && (ms == 0 || millis() - ms > 10000))
{
ms = millis();
// From the UID, claims and security rules we have set,
// it only allows us to access at examples/Access/products/user/peter/...
// Because the resource in the claim is products and group claim is user.
String path = "examples/AccessControl/products/user/";
path += app.getUid();
path += "/value";
Serial.print("Setting the int value to the granted path... ");
// This should be ok.
bool status = Database.set<int>(aClient, path, 12345);
if (status)
Serial.println("ok");
else
Firebase.printf("Error, msg: %s, code: %d\n", aClient.lastError().message().c_str(), aClient.lastError().code());
Serial.print("Setting the int value to other locations... ");
// This should be failed because we write to the path that is not allowed.
status = Database.set<int>(aClient, "/examples/Set/Int", 12345);
if (status)
Serial.println("ok");
else
Firebase.printf("Error, msg: %s, code: %d\n", aClient.lastError().message().c_str(), aClient.lastError().code());
}
}
void processData(AsyncResult &aResult)
{
// Exits when no result available when calling from the loop.
if (!aResult.isResult())
return;
if (aResult.isEvent())
{
Firebase.printf("Event task: %s, msg: %s, code: %d\n", aResult.uid().c_str(), aResult.eventLog().message().c_str(), aResult.eventLog().code());
}
if (aResult.isDebug())
{
Firebase.printf("Debug task: %s, msg: %s\n", aResult.uid().c_str(), aResult.debug().c_str());
}
if (aResult.isError())
{
Firebase.printf("Error task: %s, msg: %s, code: %d\n", aResult.uid().c_str(), aResult.error().message().c_str(), aResult.error().code());
}
if (aResult.available())
{
if (aResult.to<RealtimeDatabaseResult>().name().length())
Firebase.printf("task: %s, name: %s\n", aResult.uid().c_str(), aResult.to<RealtimeDatabaseResult>().name().c_str());
Firebase.printf("task: %s, payload: %s\n", aResult.uid().c_str(), aResult.c_str());
}
}
/**
* @param controlPath The path that the which we want to control access.
* @param readCondition The read access condition.
* @param writeCondition The write access condition.
* @param databaseSecret The database secret.
*/
bool mofifyRules(const String &controlPath, const String &readCondition, const String &writeCondition, const String &databaseSecret)
{
// Use database secret for to allow security rules access.
// The ServiceAuth (OAuth2.0 access token authorization) can also be used
// but database secret is more simple for this case.
LegacyToken legacy_token(databaseSecret);
initializeApp(aClient, app, getAuth(legacy_token));
app.getApp<RealtimeDatabase>(Database);
Database.url(DATABASE_URL);
Serial.println("Getting the security rules to check the existing conditions... ");
String jsonStr = Database.get<String>(aClient, ".settings/rules");
// If security rules are ready get.
if (aClient.lastError().code() == 0)
{
Serial.println("Success");
bool ret = true;
FirebaseJsonData parseResult;
FirebaseJson currentRules(jsonStr);
bool readConditionExists = false, writeConditionExists = false;
String rulePath = "rules";
if (controlPath.length() && controlPath[0] != '/')
rulePath += '/';
rulePath += controlPath;
// Check the read condition exists or matches
if (readCondition.length() > 0)
{
String readPath = rulePath;
readPath += "/.read";
if (currentRules.get(parseResult, readPath.c_str()) && strcmp(parseResult.to<const char *>(), readCondition.c_str()) == 0)
readConditionExists = true;
}
// Check the write condition exists or matches
if (writeCondition.length() > 0)
{
String writePath = rulePath;
writePath += "/.write";
if (currentRules.get(parseResult, writePath.c_str()) && strcmp(parseResult.to<const char *>(), writeCondition.c_str()) == 0)
writeConditionExists = true;
}
// Add conditions if they do not exist.
if (!readConditionExists || !writeConditionExists)
{
FirebaseJson addedRules;
if (!readConditionExists)
addedRules.add(".read", readCondition);
if (!writeConditionExists)
addedRules.add(".write", writeCondition);
currentRules.set(rulePath, addedRules);
String modifiedRules;
currentRules.toString(modifiedRules, true);
Serial.println("Setting the security rules to add the modified conditions... ");
bool status = Database.set<object_t>(aClient, ".settings/rules", object_t(modifiedRules));
if (status)
Serial.println("Success");
else
Firebase.printf("Error, msg: %s, code: %d\n", aClient.lastError().message().c_str(), aClient.lastError().code());
}
else
{
Serial.println("The rules exist, nothing to change");
}
currentRules.clear();
return aClient.lastError().code() == 0;
}
else
Firebase.printf("Error, msg: %s, code: %d\n", aClient.lastError().message().c_str(), aClient.lastError().code());
Serial.println("-----------------------------------");
return false;
}