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.
@@ -41,7 +41,7 @@ altitude:
41
41
  min: -500
42
42
  max: 9000
43
43
  width: 6
44
- color: black
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: purple
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: brown
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: green
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: green
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: black
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: black
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: black
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: black
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: black
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: black
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: black
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: red
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: blue
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: brown
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: green
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: black
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: blue
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: black
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: black
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: green
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: yellow
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: black
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: yellow
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: black
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: black
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: blue
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: black
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: black
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: black
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: orange
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: blue
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: blue
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: black
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: black
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: blue
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: black
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: brown
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: red
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: black
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: black
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: green
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: blue
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: black
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: blue
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: blue
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.
@@ -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.
@@ -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
- UNIQUE(`id`, `capability`, `org`)
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,