frugal-iot-server 0.3.8 → 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 +219 -61
- package/config.d/email.yaml +23 -0
- package/config.d/schema/devices.yaml +0 -2
- package/config.d/schema/modules.yaml +344 -2
- package/config.d/schema/topics.yaml +140 -46
- package/extras/aclfile +38 -0
- package/extras/mosquitto.conf +29 -0
- package/frugal-iot-createdb.sql +82 -1
- package/frugal-iot-server.js +692 -80
- 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/mailer.js +43 -0
- package/lib/replica.js +246 -0
- package/lib/resetcode.js +103 -0
- package/lib/retained.js +110 -0
- package/lib/secrets.js +148 -0
- package/package.json +12 -5
- package/public/index.html +8 -8
- package/public/service-worker.js +76 -27
- package/scripts/addbridge-pi.zsh +91 -7
- package/scripts/addbridge-prod.zsh +57 -3
- package/scripts/addorganization.zsh +5 -0
- package/scripts/clearretained.js +185 -0
- package/scripts/diagnostic.zsh +121 -2
- package/scripts/dynsec-init.js +142 -0
- package/scripts/init.zsh +64 -1
- 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/scripts/clearretained.zsh +0 -153
|
@@ -41,7 +41,7 @@ altitude:
|
|
|
41
41
|
min: -500
|
|
42
42
|
max: 9000
|
|
43
43
|
width: 6
|
|
44
|
-
color:
|
|
44
|
+
color: "#000000"
|
|
45
45
|
log: true
|
|
46
46
|
duplicates:
|
|
47
47
|
significantdate: 900000
|
|
@@ -67,7 +67,7 @@ aqi:
|
|
|
67
67
|
display: bar
|
|
68
68
|
min: 0
|
|
69
69
|
max: 5
|
|
70
|
-
color:
|
|
70
|
+
color: "#800080"
|
|
71
71
|
rw: r
|
|
72
72
|
graphable: true
|
|
73
73
|
log: true
|
|
@@ -81,7 +81,7 @@ aqi500:
|
|
|
81
81
|
display: bar
|
|
82
82
|
min: 0
|
|
83
83
|
max: 500
|
|
84
|
-
color:
|
|
84
|
+
color: "#a52a2a"
|
|
85
85
|
rw: r
|
|
86
86
|
graphable: true
|
|
87
87
|
log: true
|
|
@@ -95,7 +95,7 @@ battery:
|
|
|
95
95
|
display: text
|
|
96
96
|
min: 3000
|
|
97
97
|
max: 5000
|
|
98
|
-
color:
|
|
98
|
+
color: "#008000"
|
|
99
99
|
rw: r
|
|
100
100
|
graphable: true
|
|
101
101
|
log: true
|
|
@@ -120,7 +120,7 @@ bus:
|
|
|
120
120
|
min: 0
|
|
121
121
|
max: 32
|
|
122
122
|
width: 5
|
|
123
|
-
color:
|
|
123
|
+
color: "#008000"
|
|
124
124
|
rw: r
|
|
125
125
|
log: true
|
|
126
126
|
units: V
|
|
@@ -134,7 +134,7 @@ button:
|
|
|
134
134
|
type: bool
|
|
135
135
|
log: true
|
|
136
136
|
display: toggle
|
|
137
|
-
color:
|
|
137
|
+
color: "#000000"
|
|
138
138
|
rw: r
|
|
139
139
|
# A press changes the value, so it is still recorded - this only drops the repeats in between
|
|
140
140
|
duplicates:
|
|
@@ -155,7 +155,7 @@ controlfloat:
|
|
|
155
155
|
type: float
|
|
156
156
|
log: false
|
|
157
157
|
display: text
|
|
158
|
-
color:
|
|
158
|
+
color: "#000000"
|
|
159
159
|
wireable: true
|
|
160
160
|
rw: w
|
|
161
161
|
retain: true
|
|
@@ -166,7 +166,7 @@ controlint:
|
|
|
166
166
|
type: int
|
|
167
167
|
log: false
|
|
168
168
|
display: text
|
|
169
|
-
color:
|
|
169
|
+
color: "#000000"
|
|
170
170
|
wireable: true
|
|
171
171
|
rw: w
|
|
172
172
|
retain: true
|
|
@@ -177,7 +177,7 @@ controltext:
|
|
|
177
177
|
type: text
|
|
178
178
|
log: false
|
|
179
179
|
display: text
|
|
180
|
-
color:
|
|
180
|
+
color: "#000000"
|
|
181
181
|
wireable: true
|
|
182
182
|
rw: w
|
|
183
183
|
retain: true
|
|
@@ -186,7 +186,7 @@ controlintoggle:
|
|
|
186
186
|
type: bool
|
|
187
187
|
log: false
|
|
188
188
|
display: toggle
|
|
189
|
-
color:
|
|
189
|
+
color: "#000000"
|
|
190
190
|
rw: w
|
|
191
191
|
wireable: false
|
|
192
192
|
controlouttoggle:
|
|
@@ -194,7 +194,7 @@ controlouttoggle:
|
|
|
194
194
|
type: bool
|
|
195
195
|
log: true
|
|
196
196
|
display: toggle
|
|
197
|
-
color:
|
|
197
|
+
color: "#000000"
|
|
198
198
|
rw: r
|
|
199
199
|
wireable: true
|
|
200
200
|
# Read-only bool, so logged by default - match the "out" and "on" topics it stands in for
|
|
@@ -209,7 +209,7 @@ course:
|
|
|
209
209
|
min: 0
|
|
210
210
|
max: 360
|
|
211
211
|
width: 5
|
|
212
|
-
color:
|
|
212
|
+
color: "#000000"
|
|
213
213
|
log: true
|
|
214
214
|
duplicates:
|
|
215
215
|
significantdate: 900000
|
|
@@ -225,7 +225,7 @@ current:
|
|
|
225
225
|
min: -3200
|
|
226
226
|
max: 3200
|
|
227
227
|
width: 7
|
|
228
|
-
color:
|
|
228
|
+
color: "#ff0000"
|
|
229
229
|
rw: r
|
|
230
230
|
log: true
|
|
231
231
|
units: mA
|
|
@@ -250,7 +250,7 @@ do:
|
|
|
250
250
|
min: 0
|
|
251
251
|
max: 20
|
|
252
252
|
width: 5
|
|
253
|
-
color:
|
|
253
|
+
color: "#0000ff"
|
|
254
254
|
rw: r
|
|
255
255
|
log: true
|
|
256
256
|
units: mg/L
|
|
@@ -265,7 +265,7 @@ eco2:
|
|
|
265
265
|
display: bar
|
|
266
266
|
min: 300
|
|
267
267
|
max: 900
|
|
268
|
-
color:
|
|
268
|
+
color: "#a52a2a"
|
|
269
269
|
log: true
|
|
270
270
|
units: ppm
|
|
271
271
|
duplicates:
|
|
@@ -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
|
|
@@ -281,7 +293,7 @@ gas:
|
|
|
281
293
|
min: 0
|
|
282
294
|
max: 500
|
|
283
295
|
width: 5
|
|
284
|
-
color:
|
|
296
|
+
color: "#008000"
|
|
285
297
|
rw: r
|
|
286
298
|
log: true
|
|
287
299
|
units: kOhm
|
|
@@ -297,7 +309,7 @@ hdop:
|
|
|
297
309
|
min: 0
|
|
298
310
|
max: 50
|
|
299
311
|
width: 5
|
|
300
|
-
color:
|
|
312
|
+
color: "#000000"
|
|
301
313
|
log: false
|
|
302
314
|
duplicates:
|
|
303
315
|
significantdate: 900000
|
|
@@ -312,7 +324,7 @@ humidity:
|
|
|
312
324
|
min: 0
|
|
313
325
|
max: 100
|
|
314
326
|
width: 5
|
|
315
|
-
color:
|
|
327
|
+
color: "#0000ff"
|
|
316
328
|
log: true
|
|
317
329
|
duplicates:
|
|
318
330
|
significantdate: 900000
|
|
@@ -341,7 +353,7 @@ language_code:
|
|
|
341
353
|
type: text
|
|
342
354
|
log: false
|
|
343
355
|
display: text
|
|
344
|
-
color:
|
|
356
|
+
color: "#000000"
|
|
345
357
|
rw: r
|
|
346
358
|
lastseen:
|
|
347
359
|
leaf: lastseen
|
|
@@ -359,7 +371,7 @@ latitude:
|
|
|
359
371
|
min: -90
|
|
360
372
|
max: +90
|
|
361
373
|
width: 9
|
|
362
|
-
color:
|
|
374
|
+
color: "#000000"
|
|
363
375
|
log: true
|
|
364
376
|
duplicates:
|
|
365
377
|
significantdate: 900000
|
|
@@ -375,7 +387,7 @@ load:
|
|
|
375
387
|
min: 0
|
|
376
388
|
max: 32
|
|
377
389
|
width: 5
|
|
378
|
-
color:
|
|
390
|
+
color: "#008000"
|
|
379
391
|
rw: r
|
|
380
392
|
log: true
|
|
381
393
|
units: V
|
|
@@ -391,7 +403,7 @@ loadcell:
|
|
|
391
403
|
min: 0
|
|
392
404
|
max: 65000
|
|
393
405
|
width: 7
|
|
394
|
-
color:
|
|
406
|
+
color: "#b8860b"
|
|
395
407
|
log: true
|
|
396
408
|
duplicates:
|
|
397
409
|
significantdate: 900000
|
|
@@ -407,7 +419,7 @@ longitude:
|
|
|
407
419
|
min: -180
|
|
408
420
|
max: +180
|
|
409
421
|
width: 10
|
|
410
|
-
color:
|
|
422
|
+
color: "#000000"
|
|
411
423
|
log: true
|
|
412
424
|
duplicates:
|
|
413
425
|
significantdate: 900000
|
|
@@ -423,7 +435,7 @@ lux:
|
|
|
423
435
|
min: 0
|
|
424
436
|
max: 65000
|
|
425
437
|
width: 5
|
|
426
|
-
color:
|
|
438
|
+
color: "#b8860b"
|
|
427
439
|
log: true
|
|
428
440
|
rw: r
|
|
429
441
|
duplicates:
|
|
@@ -436,7 +448,7 @@ manual:
|
|
|
436
448
|
name: Manual
|
|
437
449
|
type: bool
|
|
438
450
|
display: toggle
|
|
439
|
-
color:
|
|
451
|
+
color: "#000000"
|
|
440
452
|
rw: r
|
|
441
453
|
log: true
|
|
442
454
|
graphable: true
|
|
@@ -452,7 +464,7 @@ message:
|
|
|
452
464
|
type: text
|
|
453
465
|
log: false
|
|
454
466
|
display: text
|
|
455
|
-
color:
|
|
467
|
+
color: "#000000"
|
|
456
468
|
wireable: true
|
|
457
469
|
rw: w
|
|
458
470
|
retain: true
|
|
@@ -464,7 +476,7 @@ mm:
|
|
|
464
476
|
min: 0
|
|
465
477
|
max: 500
|
|
466
478
|
width: 5
|
|
467
|
-
color:
|
|
479
|
+
color: "#0000ff"
|
|
468
480
|
rw: r
|
|
469
481
|
log: true
|
|
470
482
|
duplicates:
|
|
@@ -481,12 +493,23 @@ 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
|
|
487
510
|
type: bool
|
|
488
511
|
display: toggle
|
|
489
|
-
color:
|
|
512
|
+
color: "#000000"
|
|
490
513
|
rw: w
|
|
491
514
|
log: true
|
|
492
515
|
duplicates:
|
|
@@ -498,7 +521,7 @@ out:
|
|
|
498
521
|
name: Out
|
|
499
522
|
type: bool
|
|
500
523
|
display: toggle
|
|
501
|
-
color:
|
|
524
|
+
color: "#000000"
|
|
502
525
|
rw: r
|
|
503
526
|
log: true
|
|
504
527
|
wireable: true
|
|
@@ -506,13 +529,33 @@ out:
|
|
|
506
529
|
duplicates:
|
|
507
530
|
significantdate: 900000
|
|
508
531
|
significantvalue: 1
|
|
532
|
+
percent:
|
|
533
|
+
# A 0-100% figure that is worth recording and graphing - state of charge, battery health. Unlike
|
|
534
|
+
# controlfloat it is read-only and logged, so it needs its own duplicate rule; [name] and the
|
|
535
|
+
# duplicate thresholds should be overridden per module.
|
|
536
|
+
leaf: percent
|
|
537
|
+
name: Percent
|
|
538
|
+
type: float
|
|
539
|
+
display: bar
|
|
540
|
+
min: 0
|
|
541
|
+
max: 100
|
|
542
|
+
width: 3
|
|
543
|
+
color: "#008000"
|
|
544
|
+
rw: r
|
|
545
|
+
wireable: false
|
|
546
|
+
log: true
|
|
547
|
+
duplicates:
|
|
548
|
+
significantdate: 900000
|
|
549
|
+
significantvalue: 1
|
|
550
|
+
graphable: true
|
|
551
|
+
units: "%"
|
|
509
552
|
position:
|
|
510
553
|
leaf: position
|
|
511
554
|
name: Position
|
|
512
555
|
type: text
|
|
513
556
|
log: false
|
|
514
557
|
display: text
|
|
515
|
-
color:
|
|
558
|
+
color: "#000000"
|
|
516
559
|
rw: r
|
|
517
560
|
power:
|
|
518
561
|
leaf: power
|
|
@@ -522,7 +565,7 @@ power:
|
|
|
522
565
|
min: 0
|
|
523
566
|
max: 100000
|
|
524
567
|
width: 8
|
|
525
|
-
color:
|
|
568
|
+
color: "#ffa500"
|
|
526
569
|
rw: r
|
|
527
570
|
log: true
|
|
528
571
|
units: mW
|
|
@@ -539,7 +582,7 @@ pressure:
|
|
|
539
582
|
max: 1100
|
|
540
583
|
width: 6
|
|
541
584
|
units: hPa
|
|
542
|
-
color:
|
|
585
|
+
color: "#0000ff"
|
|
543
586
|
rw: r
|
|
544
587
|
log: true
|
|
545
588
|
duplicates:
|
|
@@ -554,7 +597,7 @@ rain:
|
|
|
554
597
|
min: 0
|
|
555
598
|
max: 500
|
|
556
599
|
width: 5
|
|
557
|
-
color:
|
|
600
|
+
color: "#0000ff"
|
|
558
601
|
rw: r
|
|
559
602
|
log: true
|
|
560
603
|
duplicates:
|
|
@@ -570,7 +613,7 @@ satellites:
|
|
|
570
613
|
min: 0
|
|
571
614
|
max: 32
|
|
572
615
|
width: 2
|
|
573
|
-
color:
|
|
616
|
+
color: "#000000"
|
|
574
617
|
log: false
|
|
575
618
|
duplicates:
|
|
576
619
|
significantdate: 900000
|
|
@@ -585,7 +628,7 @@ select:
|
|
|
585
628
|
log: false
|
|
586
629
|
min: 0
|
|
587
630
|
display: text
|
|
588
|
-
color:
|
|
631
|
+
color: "#000000"
|
|
589
632
|
wireable: false
|
|
590
633
|
rw: w
|
|
591
634
|
retain: true
|
|
@@ -597,7 +640,7 @@ shunt:
|
|
|
597
640
|
min: -320
|
|
598
641
|
max: 320
|
|
599
642
|
width: 6
|
|
600
|
-
color:
|
|
643
|
+
color: "#0000ff"
|
|
601
644
|
rw: r
|
|
602
645
|
log: true
|
|
603
646
|
units: mV
|
|
@@ -613,7 +656,7 @@ speed:
|
|
|
613
656
|
min: 0
|
|
614
657
|
max: 999
|
|
615
658
|
width: 5
|
|
616
|
-
color:
|
|
659
|
+
color: "#000000"
|
|
617
660
|
log: true
|
|
618
661
|
duplicates:
|
|
619
662
|
significantdate: 900000
|
|
@@ -628,7 +671,7 @@ soil:
|
|
|
628
671
|
display: bar
|
|
629
672
|
min: 0
|
|
630
673
|
max: 100
|
|
631
|
-
color:
|
|
674
|
+
color: "#a52a2a"
|
|
632
675
|
rw: r
|
|
633
676
|
log: true
|
|
634
677
|
duplicates:
|
|
@@ -637,6 +680,22 @@ soil:
|
|
|
637
680
|
graphable: true
|
|
638
681
|
calibrate: true
|
|
639
682
|
units: "%"
|
|
683
|
+
tank:
|
|
684
|
+
leaf: tank
|
|
685
|
+
name: Water Tank
|
|
686
|
+
type: float
|
|
687
|
+
display: bar
|
|
688
|
+
min: 0
|
|
689
|
+
max: 100
|
|
690
|
+
width: 5
|
|
691
|
+
color: "#0000ff"
|
|
692
|
+
rw: r
|
|
693
|
+
log: true
|
|
694
|
+
duplicates:
|
|
695
|
+
significantdate: 900000
|
|
696
|
+
significantvalue: 1
|
|
697
|
+
graphable: true
|
|
698
|
+
units: "%"
|
|
640
699
|
temperature:
|
|
641
700
|
leaf: temperature
|
|
642
701
|
name: Temperature
|
|
@@ -646,7 +705,7 @@ temperature:
|
|
|
646
705
|
min: 0
|
|
647
706
|
max: 50
|
|
648
707
|
width: 4
|
|
649
|
-
color:
|
|
708
|
+
color: "#ff0000"
|
|
650
709
|
wireable: false
|
|
651
710
|
log: true
|
|
652
711
|
units: Cel
|
|
@@ -663,7 +722,7 @@ timeoff:
|
|
|
663
722
|
type: float
|
|
664
723
|
log: false
|
|
665
724
|
display: text
|
|
666
|
-
color:
|
|
725
|
+
color: "#000000"
|
|
667
726
|
rw: w
|
|
668
727
|
wireable: true
|
|
669
728
|
units: s
|
|
@@ -676,7 +735,7 @@ timeon:
|
|
|
676
735
|
type: float
|
|
677
736
|
log: false
|
|
678
737
|
display: text
|
|
679
|
-
color:
|
|
738
|
+
color: "#000000"
|
|
680
739
|
rw: w
|
|
681
740
|
wireable: true
|
|
682
741
|
units: s
|
|
@@ -687,7 +746,7 @@ tvoc:
|
|
|
687
746
|
display: bar
|
|
688
747
|
min: 0
|
|
689
748
|
max: 99
|
|
690
|
-
color:
|
|
749
|
+
color: "#008000"
|
|
691
750
|
rw: r
|
|
692
751
|
log: true
|
|
693
752
|
units: ppb
|
|
@@ -703,7 +762,7 @@ ultrasonic:
|
|
|
703
762
|
min: 0
|
|
704
763
|
max: 7500
|
|
705
764
|
width: 4
|
|
706
|
-
color:
|
|
765
|
+
color: "#0000ff"
|
|
707
766
|
rw: r
|
|
708
767
|
log: true
|
|
709
768
|
units: mm
|
|
@@ -718,8 +777,43 @@ utc_time:
|
|
|
718
777
|
type: text
|
|
719
778
|
log: false
|
|
720
779
|
display: text
|
|
721
|
-
color:
|
|
780
|
+
color: "#000000"
|
|
781
|
+
rw: r
|
|
782
|
+
voltage:
|
|
783
|
+
# A DC voltage read through a resistor divider - see Sensor_Voltage in the library. Millivolts,
|
|
784
|
+
# like `battery`, but without that topic's single-lithium-cell range or its special-casing in
|
|
785
|
+
# the UX. [name, min, max] should be overridden per module.
|
|
786
|
+
leaf: voltage
|
|
787
|
+
name: Voltage
|
|
788
|
+
type: int
|
|
789
|
+
display: text
|
|
790
|
+
min: 0
|
|
791
|
+
max: 30000
|
|
792
|
+
color: "#800080"
|
|
722
793
|
rw: r
|
|
794
|
+
graphable: true
|
|
795
|
+
log: true
|
|
796
|
+
units: mV
|
|
797
|
+
duplicates:
|
|
798
|
+
significantdate: 900000
|
|
799
|
+
significantvalue: 100
|
|
800
|
+
volts:
|
|
801
|
+
leaf: volts
|
|
802
|
+
name: Volts
|
|
803
|
+
type: float
|
|
804
|
+
display: slider
|
|
805
|
+
rw: w
|
|
806
|
+
min: 0
|
|
807
|
+
max: 3.3
|
|
808
|
+
width: 4
|
|
809
|
+
color: "#800080"
|
|
810
|
+
units: V
|
|
811
|
+
wireable: true
|
|
812
|
+
log: true
|
|
813
|
+
duplicates:
|
|
814
|
+
significantdate: 900000
|
|
815
|
+
significantvalue: 0.05
|
|
816
|
+
graphable: true
|
|
723
817
|
wifibars:
|
|
724
818
|
leaf: wifibars
|
|
725
819
|
name: WiFi
|
|
@@ -728,7 +822,7 @@ wifibars:
|
|
|
728
822
|
display: text
|
|
729
823
|
min: 0
|
|
730
824
|
max: 4
|
|
731
|
-
color:
|
|
825
|
+
color: "#0000ff"
|
|
732
826
|
rw: r
|
|
733
827
|
# Logged, because an int that is read-only is logged unless told otherwise. Only 5 values, so
|
|
734
828
|
# any change is worth recording and nothing else is
|
|
@@ -741,5 +835,5 @@ wifissid:
|
|
|
741
835
|
type: text
|
|
742
836
|
log: false
|
|
743
837
|
display: text
|
|
744
|
-
color:
|
|
838
|
+
color: "#0000ff"
|
|
745
839
|
rw: r
|
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,
|