frugal-iot-server 0.3.9 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALLATION.md +77 -18
- package/config.d/schema/modules.yaml +15 -0
- package/config.d/schema/topics.yaml +23 -0
- package/extras/aclfile +38 -0
- package/extras/mosquitto.conf +29 -0
- package/frugal-iot-createdb.sql +82 -1
- package/frugal-iot-server.js +552 -72
- package/lib/api-routes.js +7 -7
- package/lib/config-for-user.js +115 -0
- package/lib/dynsec-plan.js +246 -0
- package/lib/dynsec-server.js +204 -0
- package/lib/dynsec-sync.js +209 -0
- package/lib/dynsec.js +166 -0
- package/lib/enrol.js +288 -0
- package/lib/replica.js +246 -0
- package/lib/retained.js +110 -0
- package/lib/secrets.js +148 -0
- package/package.json +8 -4
- package/public/index.html +0 -8
- package/public/service-worker.js +51 -1
- package/scripts/addbridge-pi.zsh +56 -2
- package/scripts/addbridge-prod.zsh +48 -0
- package/scripts/addorganization.zsh +5 -0
- package/scripts/diagnostic.zsh +121 -2
- package/scripts/dynsec-init.js +142 -0
- package/scripts/init.zsh +63 -0
- package/scripts/install-pi.sh +83 -4
- package/scripts/migrate-permissions-project.sql +21 -0
- package/scripts/not-published.txt +5 -0
- package/scripts/rebuild-dynsec.js +190 -0
- package/scripts/resetnode.js +69 -0
package/INSTALLATION.md
CHANGED
|
@@ -503,9 +503,9 @@ npx --no frugal-iot-addorganization myfarm "My Farm" you@example.com +6112345678
|
|
|
503
503
|
|
|
504
504
|
The arguments are: organization id, display name, your email, your phone (`+` and digits only),
|
|
505
505
|
and a password. That one command writes `config.d/organizations/myfarm.yaml`, creates a login account
|
|
506
|
-
named after the organization (`myfarm`), grants it admin rights, creates its OTA
|
|
507
|
-
the organization's account to the broker's password file** — which is why the
|
|
508
|
-
installed first.
|
|
506
|
+
named after the organization (`myfarm`), grants it admin, read and write rights, creates its OTA
|
|
507
|
+
directory, **and adds the organization's account to the broker's password file** — which is why the
|
|
508
|
+
broker had to be installed first.
|
|
509
509
|
|
|
510
510
|
* **Organization id `myfarm`** — must be 1–10 lower-case letters or digits. It becomes the first part
|
|
511
511
|
of every MQTT topic, so it must match what your sensor nodes are configured to publish to.
|
|
@@ -833,6 +833,15 @@ in the instant the link returned. A gap is honest; that would not be. Controls a
|
|
|
833
833
|
other way round, and *are* queued, so turning something on from production while the Pi is offline
|
|
834
834
|
takes effect when it reconnects.
|
|
835
835
|
|
|
836
|
+
**People, as well as readings.** A bridge relays topics, not accounts, so on its own it does not let
|
|
837
|
+
someone registered on production log in here. Step 11b also prints a *replica token*, and step 11c
|
|
838
|
+
asks for it; with it, this Pi pulls that organization's logins and permissions from production every
|
|
839
|
+
fifteen minutes, so the same people can use this Pi's dashboard — including while production is
|
|
840
|
+
unreachable, which is the point of the Pi. It is optional: leave the token blank and the bridge
|
|
841
|
+
relays readings as before. Nothing derived from a password travels either way, and each server
|
|
842
|
+
issues its own broker credentials, so the same person has different ones here and there. Revoking a
|
|
843
|
+
permission on production removes it here on the next pull. See SECURITY.md.
|
|
844
|
+
|
|
836
845
|
#### 11a. Preparing a production server to accept bridges
|
|
837
846
|
|
|
838
847
|
Once per production server, not per Pi — and if bridging to `frugaliot.naturalinnovation.org`
|
|
@@ -908,12 +917,16 @@ installed by hand, the written procedure for doing that.
|
|
|
908
917
|
> Note the asymmetry with the Pi end below: a *certificate* change needs only `reload`, but adding
|
|
909
918
|
> or changing a *bridge* needs a full `restart`, because Mosquitto does not reload bridges.
|
|
910
919
|
|
|
911
|
-
**Turn on access control
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
920
|
+
**Turn on access control** — if this broker predates the release that ships it. Since
|
|
921
|
+
`extras/aclfile` became part of the base install, a Pi set up with `install-pi.sh` already has this
|
|
922
|
+
and there is nothing to do here; the steps below are for a broker built before that, or one whose
|
|
923
|
+
configuration was written by hand.
|
|
924
|
+
|
|
925
|
+
Without it, any account that can log in to the broker can publish and subscribe anywhere on it, so
|
|
926
|
+
a per-Pi account would be no more confined than the organization's own. This is the part to plan
|
|
927
|
+
carefully, because Mosquitto's ACL file is **deny-by-default**: the moment the broker names an
|
|
928
|
+
`acl_file`, every existing account with no entry in it stops working. So the file that restricts new
|
|
929
|
+
bridge accounts has to grant the existing organizations what they already have, in the same change.
|
|
917
930
|
|
|
918
931
|
```
|
|
919
932
|
sudo tee /etc/mosquitto/aclfile >/dev/null <<'EOF'
|
|
@@ -923,12 +936,21 @@ sudo tee /etc/mosquitto/aclfile >/dev/null <<'EOF'
|
|
|
923
936
|
# first element of every Frugal IoT topic is the organization id - so this is what the nodes,
|
|
924
937
|
# dashboards and the server's own logger already do. It just stops being optional.
|
|
925
938
|
pattern readwrite %u/#
|
|
939
|
+
|
|
940
|
+
# Whether a bridge is up, for frugal-iot-diagnostic. "pattern readwrite %u/#" does not match $SYS,
|
|
941
|
+
# so without this the diagnostic reports every bridge as never having connected. Mosquitto logs a
|
|
942
|
+
# warning that this pattern contains no %u - harmless, and a "topic" line would apply to anonymous
|
|
943
|
+
# clients only, which is not what is wanted.
|
|
944
|
+
pattern read $SYS/broker/connection/+/state
|
|
926
945
|
EOF
|
|
927
|
-
sudo
|
|
946
|
+
sudo chown mosquitto:mosquitto /etc/mosquitto/aclfile
|
|
947
|
+
sudo chmod 600 /etc/mosquitto/aclfile
|
|
928
948
|
```
|
|
929
949
|
|
|
930
|
-
|
|
931
|
-
to the `mosquitto` user, so a root-only file stops it starting.
|
|
950
|
+
The ownership matters: like the password file and the TLS key, the broker reads this *after*
|
|
951
|
+
dropping to the `mosquitto` user, so a root-only file stops it starting. Mode 600 owned by
|
|
952
|
+
`mosquitto` rather than 644 owned by root — 2.0.21 accepts the latter but warns on every start that
|
|
953
|
+
a file it does not own, or one that is world readable, "will be refused by a future version". Then name it in the configuration
|
|
932
954
|
and restart:
|
|
933
955
|
|
|
934
956
|
```
|
|
@@ -949,10 +971,12 @@ out, delete `/etc/mosquitto/conf.d/zy-frugal-iot-acl.conf` and restart.
|
|
|
949
971
|
|
|
950
972
|
Nothing needs adding here per Pi: `frugal-iot-addbridge-prod` in 11b appends each bridge's own rule.
|
|
951
973
|
|
|
952
|
-
>
|
|
953
|
-
>
|
|
954
|
-
>
|
|
955
|
-
>
|
|
974
|
+
> `pattern readwrite %u/#` on its own denies `$SYS`, which would stop `frugal-iot-diagnostic`
|
|
975
|
+
> reporting whether a bridge is connected — it reads `$SYS/broker/connection/+/state` as the
|
|
976
|
+
> organization. The second rule above restores exactly that one topic and nothing else. Reading all
|
|
977
|
+
> of `$SYS` would also work and would hand every account the broker's client counts, subscription
|
|
978
|
+
> counts and traffic totals, which on a multi-organization broker tells each organization about the
|
|
979
|
+
> others.
|
|
956
980
|
|
|
957
981
|
#### 11b. Authorizing this Pi (run on the production server)
|
|
958
982
|
|
|
@@ -1204,10 +1228,19 @@ command for each. That matters most for the files you copy somewhere else — a
|
|
|
1204
1228
|
`extras/mosquitto.conf` or `extras/frugaliot.service` does nothing until you install it again:
|
|
1205
1229
|
|
|
1206
1230
|
```
|
|
1207
|
-
sudo cp extras/mosquitto.conf /etc/mosquitto/conf.d/frugal-iot.conf
|
|
1208
|
-
sudo cp extras/
|
|
1231
|
+
sudo cp node_modules/frugal-iot-server/extras/mosquitto.conf /etc/mosquitto/conf.d/frugal-iot.conf
|
|
1232
|
+
sudo cp node_modules/frugal-iot-server/extras/aclfile /etc/mosquitto/aclfile
|
|
1233
|
+
sudo chown mosquitto:mosquitto /etc/mosquitto/aclfile && sudo chmod 600 /etc/mosquitto/aclfile
|
|
1234
|
+
sudo systemctl restart mosquitto
|
|
1235
|
+
sudo cp node_modules/frugal-iot-server/extras/frugaliot.service /etc/systemd/system/ && sudo systemctl daemon-reload && sudo systemctl restart frugaliot
|
|
1209
1236
|
```
|
|
1210
1237
|
|
|
1238
|
+
Note those read from `node_modules/frugal-iot-server/extras/`, **not** from `extras/` in this
|
|
1239
|
+
directory. `frugal-iot-init` keeps your local copy and only tells you it differs, so the local one is
|
|
1240
|
+
the *old* version — copying it would reinstall what you already have. Re-running the install script
|
|
1241
|
+
(`bash node_modules/frugal-iot-server/scripts/install-pi.sh`) does the same job and reads from the
|
|
1242
|
+
same place.
|
|
1243
|
+
|
|
1211
1244
|
> It does not compare `config.yaml`, `config.d/mqtt.yaml`, `config.d/logger.yaml` or
|
|
1212
1245
|
> `config.d/server.yaml`, because those hold your own settings and would differ every time. If a
|
|
1213
1246
|
> release note mentions a new setting in one of them, compare it yourself:
|
|
@@ -1223,6 +1256,32 @@ do. On a Pi, compare the two files above and copy across the settings you want.
|
|
|
1223
1256
|
Your organizations, accounts, database and logged data are untouched by an upgrade — they live in
|
|
1224
1257
|
this directory, not in `node_modules`.
|
|
1225
1258
|
|
|
1259
|
+
### One thing to do by hand after upgrading: grant WRITE
|
|
1260
|
+
|
|
1261
|
+
Commanding a device through the API (`/devices/action`, `/devices/property`) now requires a **WRITE**
|
|
1262
|
+
permission, which is separate from READ and is *not* implied by ADMIN. New organizations get it
|
|
1263
|
+
automatically, but an upgrade does not touch your database, so on an existing server nobody has it
|
|
1264
|
+
yet and switching a relay on answers 401.
|
|
1265
|
+
|
|
1266
|
+
Grant it from the dashboard — **Admin → People**, pick the person, choose `WRITE` — or in one
|
|
1267
|
+
statement, to everyone who already administers an organization:
|
|
1268
|
+
|
|
1269
|
+
```
|
|
1270
|
+
sqlite3 frugal-iot.db "INSERT OR IGNORE INTO permissions (id, capability, org)
|
|
1271
|
+
SELECT id, 'WRITE', org FROM permissions WHERE capability = 'ADMIN';"
|
|
1272
|
+
sudo systemctl restart frugaliot
|
|
1273
|
+
```
|
|
1274
|
+
|
|
1275
|
+
Check who has it:
|
|
1276
|
+
|
|
1277
|
+
```
|
|
1278
|
+
sqlite3 frugal-iot.db "SELECT id, org FROM permissions WHERE capability = 'WRITE';"
|
|
1279
|
+
```
|
|
1280
|
+
|
|
1281
|
+
Do **not** grant WRITE to id `0`. That row means "every logged-in account", including anyone who has
|
|
1282
|
+
just registered themselves, and handing them write access is exactly what the WRITE check exists to
|
|
1283
|
+
prevent. (Read is a different matter — see the `(0, 'READ', ...)` rows, which are there on purpose.)
|
|
1284
|
+
|
|
1226
1285
|
---
|
|
1227
1286
|
|
|
1228
1287
|
## Open questions
|
|
@@ -523,11 +523,16 @@ soc:
|
|
|
523
523
|
name: Battery
|
|
524
524
|
min: 0
|
|
525
525
|
max: 16000
|
|
526
|
+
# controlfloat is "log: false" because most control inputs are wired from a topic that is
|
|
527
|
+
# already logged in its own right. Battery and panel are not - the node measures and
|
|
528
|
+
# publishes them here and nowhere else, so without this they were never archived.
|
|
529
|
+
log: true
|
|
526
530
|
- leaf: panel
|
|
527
531
|
leaf_from: controlfloat
|
|
528
532
|
name: Panel
|
|
529
533
|
min: 0
|
|
530
534
|
max: 30000
|
|
535
|
+
log: true
|
|
531
536
|
- leaf: profile
|
|
532
537
|
leaf_from: controlint
|
|
533
538
|
name: Battery type
|
|
@@ -554,6 +559,16 @@ soilmodbus:
|
|
|
554
559
|
- leaf: temperature
|
|
555
560
|
name: "Soil Temperature"
|
|
556
561
|
color: brown
|
|
562
|
+
# The node's clock - what time it thinks it is and how far from UTC it displays. Infrastructure
|
|
563
|
+
# rather than a sensor, so it belongs in the Frugal_IoT drop-down alongside ota, and neither leaf is
|
|
564
|
+
# logged. Without this entry both leaves resolved to no schema at all, so every message from them
|
|
565
|
+
# reached valueFromText with no type and was reported as "Unrecognized message type: undefined".
|
|
566
|
+
time:
|
|
567
|
+
name: Time
|
|
568
|
+
insidefrugaliot: true
|
|
569
|
+
topics:
|
|
570
|
+
- leaf: epoch
|
|
571
|
+
- leaf: offset
|
|
557
572
|
ultrasonic:
|
|
558
573
|
name: "Ultrasonic Distance"
|
|
559
574
|
topics:
|
|
@@ -273,6 +273,18 @@ eco2:
|
|
|
273
273
|
significantvalue: 5
|
|
274
274
|
rw: r
|
|
275
275
|
graphable: true
|
|
276
|
+
# Unix time in seconds, as the node's clock currently reads it - published by the "time" module so
|
|
277
|
+
# the client can see how far a node has drifted. A clock reading rather than a measurement: logging
|
|
278
|
+
# it would fill the CSVs with a number that only ever counts up.
|
|
279
|
+
epoch:
|
|
280
|
+
leaf: epoch
|
|
281
|
+
name: Epoch
|
|
282
|
+
type: int
|
|
283
|
+
log: false
|
|
284
|
+
display: text
|
|
285
|
+
color: "#000000"
|
|
286
|
+
rw: r
|
|
287
|
+
units: s
|
|
276
288
|
gas:
|
|
277
289
|
leaf: gas
|
|
278
290
|
name: Gas Resistance
|
|
@@ -481,6 +493,17 @@ name:
|
|
|
481
493
|
display: text
|
|
482
494
|
rw: w
|
|
483
495
|
retain: true
|
|
496
|
+
# How far the node is displaying from UTC, so it can show local time. Like epoch, a setting the node
|
|
497
|
+
# reports back rather than anything measured, so not logged.
|
|
498
|
+
offset:
|
|
499
|
+
leaf: offset
|
|
500
|
+
name: UTC Offset
|
|
501
|
+
type: int
|
|
502
|
+
log: false
|
|
503
|
+
display: text
|
|
504
|
+
color: "#000000"
|
|
505
|
+
rw: r
|
|
506
|
+
units: min
|
|
484
507
|
on:
|
|
485
508
|
leaf: on
|
|
486
509
|
name: On
|
package/extras/aclfile
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Frugal IoT access control. Named by acl_file in extras/mosquitto.conf.
|
|
2
|
+
#
|
|
3
|
+
# DENY BY DEFAULT: the moment the broker names an acl_file, an account with no rule here can still
|
|
4
|
+
# log in but can reach no topic at all. So adding an account means adding its rule in the same
|
|
5
|
+
# change - see scripts/addbridge-prod.zsh, which appends one per Pi.
|
|
6
|
+
#
|
|
7
|
+
# This file is read AFTER mosquitto drops privileges, so it must be readable by the mosquitto user.
|
|
8
|
+
# Install it as mosquitto:mosquitto mode 600 - NOT root-owned 644. Mosquitto 2.0.21 warns
|
|
9
|
+
# "owner is not mosquitto ... Future versions will refuse to load this file" and separately
|
|
10
|
+
# "has world readable permissions", so 644 root works today and is on notice to stop working. The
|
|
11
|
+
# rule is the same one the password file follows: owned by whoever opens it.
|
|
12
|
+
|
|
13
|
+
# Each organization reaches only its own topic tree. "%u" is the connecting account name, and the
|
|
14
|
+
# first element of every Frugal IoT topic is the organization id - so this is what the nodes,
|
|
15
|
+
# dashboards and the server's own logger already do. It just stops being optional.
|
|
16
|
+
pattern readwrite %u/#
|
|
17
|
+
|
|
18
|
+
# Whether a bridge is up. With "notifications" left on (the default) a bridge publishes a retained
|
|
19
|
+
# 1 or 0 to $SYS/broker/connection/<remote_clientid>/state, and that is what frugal-iot-diagnostic
|
|
20
|
+
# reads to say whether a site is connected.
|
|
21
|
+
#
|
|
22
|
+
# Needed because "pattern readwrite %u/#" does not match $SYS, so without this line the diagnostic
|
|
23
|
+
# reports every bridge as never having connected - measured, not guessed. This is the narrowest rule
|
|
24
|
+
# that fixes it: it exposes which bridges exist and whether each is up, and nothing else. Reading
|
|
25
|
+
# all of $SYS ("pattern read $SYS/#") would also work and would hand every account the broker's
|
|
26
|
+
# client counts, subscription counts and traffic totals, which on a multi-organization broker tells
|
|
27
|
+
# each organization about the others.
|
|
28
|
+
#
|
|
29
|
+
# Mosquitto logs "Warning: ACL pattern ... does not contain '%c' or '%u'" for the line below, on
|
|
30
|
+
# every start and reload. It is harmless and expected: a "pattern" line is the only way to say
|
|
31
|
+
# "every authenticated account", because a bare "topic" line applies to anonymous clients only.
|
|
32
|
+
# Do not "fix" it by changing it to a topic line - that would silently grant nothing.
|
|
33
|
+
pattern read $SYS/broker/connection/+/state
|
|
34
|
+
|
|
35
|
+
# Bridge connections are exempt from this file on their local broker - a bridge with no
|
|
36
|
+
# local_username still carries traffic in both directions with the rules above in force (measured on
|
|
37
|
+
# mosquitto 2.0.21). So a Pi's own bridge needs nothing here; it is the far end, on production, that
|
|
38
|
+
# needs a rule, and addbridge-prod.zsh writes it.
|
package/extras/mosquitto.conf
CHANGED
|
@@ -9,6 +9,35 @@ protocol websockets
|
|
|
9
9
|
# directory has to be writable by that same user. See INSTALLATION.md step A5.
|
|
10
10
|
password_file /var/lib/mosquitto/passwords
|
|
11
11
|
|
|
12
|
+
# Which topics each account may reach. Without this, every account that can log in can publish and
|
|
13
|
+
# subscribe anywhere on the broker, across all organizations - so an organization's own password,
|
|
14
|
+
# which is handed to every node and every logged-in browser, would reach every other organization's
|
|
15
|
+
# data too.
|
|
16
|
+
#
|
|
17
|
+
# DENY BY DEFAULT: naming this file stops any account that has no rule in it. The shipped file
|
|
18
|
+
# grants each organization its own topic tree, which is all the nodes, dashboards and logger use, so
|
|
19
|
+
# nothing should notice. But an account added by hand needs a rule adding by hand.
|
|
20
|
+
#
|
|
21
|
+
# Installed from extras/aclfile - which must be in place BEFORE this line takes effect, or the
|
|
22
|
+
# broker will not start.
|
|
23
|
+
acl_file /etc/mosquitto/aclfile
|
|
24
|
+
|
|
25
|
+
# Accounts and permissions the server can create and remove while the broker is running, over
|
|
26
|
+
# $CONTROL/dynamic-security/v1. This is what lets each user and each node have its own credential
|
|
27
|
+
# rather than sharing the organization's - see SECURITY-REVIEW.md.
|
|
28
|
+
#
|
|
29
|
+
# The plugin's path is set by the installer, because it varies by distribution: Debian and
|
|
30
|
+
# Raspberry Pi OS put it in /usr/lib/<architecture>/, others in a mosquitto/ subdirectory.
|
|
31
|
+
#
|
|
32
|
+
# Both files must exist before this takes effect or the broker will not start. The state file is
|
|
33
|
+
# created by "mosquitto_ctrl dynsec init", which install-pi.sh does.
|
|
34
|
+
#
|
|
35
|
+
# Plugin checks run BEFORE the built-in password_file and acl_file checks, and an account dynsec
|
|
36
|
+
# does not know is passed through to them - measured on 2.0.21, and what makes it possible to move
|
|
37
|
+
# accounts across one at a time rather than all at once.
|
|
38
|
+
plugin PLUGIN_PATH_SET_BY_INSTALLER
|
|
39
|
+
plugin_opt_config_file /var/lib/mosquitto/dynamic-security.json
|
|
40
|
+
|
|
12
41
|
# A node on a weak WiFi signal reconnects often, and each connection and disconnection would be a
|
|
13
42
|
# line in /var/log/mosquitto/mosquitto.log. On an SD card those writes wear the card out, and
|
|
14
43
|
# nothing is lost by dropping them, since the readings themselves are recorded by the server.
|
package/frugal-iot-createdb.sql
CHANGED
|
@@ -20,12 +20,93 @@ CREATE TABLE IF NOT EXISTS `users` (
|
|
|
20
20
|
`email` TEXT,
|
|
21
21
|
`phone` TEXT
|
|
22
22
|
);
|
|
23
|
+
-- `project` scopes a permission to one project of an organization. Empty string means the whole
|
|
24
|
+
-- organization, which is how every row created before this column existed behaves.
|
|
25
|
+
--
|
|
26
|
+
-- NOT NULL DEFAULT '' rather than nullable, and that matters: SQLite treats NULLs as DISTINCT in a
|
|
27
|
+
-- UNIQUE constraint, so with a nullable column `(2,'READ','dev',NULL)` could be inserted twice and
|
|
28
|
+
-- the constraint that used to prevent duplicate organization-wide rows would silently stop working.
|
|
29
|
+
-- An empty string compares equal to itself, so uniqueness holds. It also keeps the queries simple:
|
|
30
|
+
-- `project = ?` everywhere, never `IS NULL`.
|
|
23
31
|
CREATE TABLE IF NOT EXISTS `permissions` (
|
|
24
32
|
`id` INTEGER NOT NULL,
|
|
25
33
|
`capability` TEXT NOT NULL,
|
|
26
34
|
`org` TEXT NOT NULL,
|
|
27
|
-
|
|
35
|
+
`project` TEXT NOT NULL DEFAULT '',
|
|
36
|
+
UNIQUE(`id`, `capability`, `org`, `project`)
|
|
37
|
+
);
|
|
38
|
+
-- One row per node that has enrolled, holding the broker credential it was issued.
|
|
39
|
+
--
|
|
40
|
+
-- The password is stored, not derived: a node keeps its copy in LittleFS and cannot recompute
|
|
41
|
+
-- anything, so deriving it from a secret would mean that losing the secret strands the whole fleet.
|
|
42
|
+
-- Storing it here is also what makes "the database is the source of truth, and dynsec can be
|
|
43
|
+
-- rebuilt from it" true for nodes as well as users.
|
|
44
|
+
--
|
|
45
|
+
-- No last_seen column on purpose: the logger already tracks that in memory and serves it through
|
|
46
|
+
-- reportNodes(), and writing it here would be a database write per reading - the SD-card wear the
|
|
47
|
+
-- logger's own buffering exists to avoid.
|
|
48
|
+
--
|
|
49
|
+
-- `lora` records whether the firmware was built with LoRaMesher, declared at enrolment. It decides
|
|
50
|
+
-- whether the node joins <org>-gateways, which grants the broader publish a gateway cannot avoid
|
|
51
|
+
-- needing (it republishes other nodes' readings under its own account). Scoped by what the build
|
|
52
|
+
-- can do rather than by what it is doing at the time, because any node that sees WiFi can promote
|
|
53
|
+
-- itself to gateway at runtime.
|
|
54
|
+
CREATE TABLE IF NOT EXISTS `nodes` (
|
|
55
|
+
`org` TEXT NOT NULL,
|
|
56
|
+
`project` TEXT NOT NULL,
|
|
57
|
+
`nodeid` TEXT NOT NULL,
|
|
58
|
+
`password` TEXT NOT NULL,
|
|
59
|
+
`lora` INTEGER NOT NULL DEFAULT 0,
|
|
60
|
+
`enrolled_at` INTEGER NOT NULL,
|
|
61
|
+
UNIQUE(`org`, `project`, `nodeid`)
|
|
28
62
|
);
|
|
63
|
+
|
|
64
|
+
-- A Pi authorised to bridge into this server, and the token it pulls users with (SECURITY.md S11).
|
|
65
|
+
--
|
|
66
|
+
-- A bridge relays topics, not accounts, so a person who logs into this server has no account on the
|
|
67
|
+
-- Pi's broker. GET /replica/:org lets the Pi fetch the logins and permissions for the organizations
|
|
68
|
+
-- it hosts, so the same people can log in there - including when this server is unreachable, which
|
|
69
|
+
-- is the reason the Pi exists.
|
|
70
|
+
--
|
|
71
|
+
-- Nothing derived from a password travels: the Pi authenticates the login itself against the
|
|
72
|
+
-- replicated hash and derives its own broker credential from its own user_secret, so the two
|
|
73
|
+
-- brokers issue different passwords for the same person and neither secret is shared.
|
|
74
|
+
--
|
|
75
|
+
-- One token per Pi per organization, so one site can be revoked without disturbing another. The
|
|
76
|
+
-- token is a bearer credential for reading that organization's logins - not for changing anything.
|
|
77
|
+
CREATE TABLE IF NOT EXISTS `bridges` (
|
|
78
|
+
`org` TEXT NOT NULL,
|
|
79
|
+
`site` TEXT NOT NULL,
|
|
80
|
+
`token` TEXT NOT NULL,
|
|
81
|
+
`created_at` INTEGER NOT NULL,
|
|
82
|
+
`last_pull` INTEGER,
|
|
83
|
+
UNIQUE(`org`, `site`)
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
-- An admin's decision about one node, for the Enrolled column on the dashboard's Nodes card
|
|
87
|
+
-- (SECURITY.md S12). Two states are worth storing:
|
|
88
|
+
--
|
|
89
|
+
-- approved the next enrolment request from this node id is accepted whatever secret it presents,
|
|
90
|
+
-- and without having to prove it holds a credential - for a node whose secret was
|
|
91
|
+
-- withdrawn, or that was flashed with none, and which cannot be reached physically.
|
|
92
|
+
-- Consumed on use, so an approval admits one node once.
|
|
93
|
+
-- denied its broker account is deleted and enrolment is refused - the kill switch for a node
|
|
94
|
+
-- publishing bad readings.
|
|
95
|
+
--
|
|
96
|
+
-- Keyed by (org, nodeid), NOT by project: a node states its own project, so keying on it would let
|
|
97
|
+
-- a denied node return by claiming a different one.
|
|
98
|
+
--
|
|
99
|
+
-- Persisted, unlike the record of failed attempts, because a restart must not un-deny a node.
|
|
100
|
+
CREATE TABLE IF NOT EXISTS `node_grants` (
|
|
101
|
+
`org` TEXT NOT NULL,
|
|
102
|
+
`project` TEXT NOT NULL DEFAULT '',
|
|
103
|
+
`nodeid` TEXT NOT NULL,
|
|
104
|
+
`state` TEXT NOT NULL,
|
|
105
|
+
`created_by` TEXT NOT NULL DEFAULT '',
|
|
106
|
+
`created_at` INTEGER NOT NULL,
|
|
107
|
+
UNIQUE(`org`, `nodeid`)
|
|
108
|
+
);
|
|
109
|
+
|
|
29
110
|
CREATE TABLE IF NOT EXISTS `projects` (
|
|
30
111
|
`org` TEXT NOT NULL,
|
|
31
112
|
`id` TEXT NOT NULL,
|