RBAC (#102)
Closes #14 Reviewed-on: #102 Co-authored-by: Daniel Ponte <amigan@gmail.com> Co-committed-by: Daniel Ponte <amigan@gmail.com>
This commit is contained in:
parent
66dc6e4b78
commit
dea092d448
51 changed files with 2593 additions and 346 deletions
|
@ -1,4 +1,4 @@
|
||||||
dir: '{{ replaceAll .InterfaceDirRelative "internal" "internal_" }}/mocks'
|
dir: '{{.InterfaceDir}}/mocks'
|
||||||
mockname: "{{.InterfaceName}}"
|
mockname: "{{.InterfaceName}}"
|
||||||
outpkg: "mocks"
|
outpkg: "mocks"
|
||||||
filename: "{{.InterfaceName}}.go"
|
filename: "{{.InterfaceName}}.go"
|
||||||
|
@ -9,3 +9,7 @@ packages:
|
||||||
interfaces:
|
interfaces:
|
||||||
Store:
|
Store:
|
||||||
DBTX:
|
DBTX:
|
||||||
|
dynatron.me/x/stillbox/pkg/rbac:
|
||||||
|
config:
|
||||||
|
interfaces:
|
||||||
|
RBAC:
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -39,6 +39,7 @@ require (
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
|
||||||
|
github.com/el-mike/restrict/v2 v2.0.0 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||||
github.com/go-audio/audio v1.0.0 // indirect
|
github.com/go-audio/audio v1.0.0 // indirect
|
||||||
github.com/go-audio/riff v1.0.0 // indirect
|
github.com/go-audio/riff v1.0.0 // indirect
|
||||||
|
@ -56,6 +57,7 @@ require (
|
||||||
github.com/lestrrat-go/iter v1.0.2 // indirect
|
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||||
github.com/lestrrat-go/jwx/v2 v2.1.3 // indirect
|
github.com/lestrrat-go/jwx/v2 v2.1.3 // indirect
|
||||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||||
|
github.com/matoous/go-nanoid v1.5.1 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||||
|
|
20
go.sum
20
go.sum
|
@ -8,6 +8,12 @@ github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||||
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA=
|
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA=
|
||||||
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
|
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
|
||||||
|
github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I=
|
||||||
|
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||||
|
github.com/casbin/casbin/v2 v2.103.0 h1:dHElatNXNrr8XcseUov0ZSiWjauwmZZE6YMV3eU1yic=
|
||||||
|
github.com/casbin/casbin/v2 v2.103.0/go.mod h1:Ee33aqGrmES+GNL17L0h9X28wXuo829wnNUnS0edAco=
|
||||||
|
github.com/casbin/govaluate v1.3.0 h1:VA0eSY0M2lA86dYd5kPPuNZMUD9QkWnOCnavGrw9myc=
|
||||||
|
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
|
@ -28,6 +34,8 @@ github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj
|
||||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
|
github.com/el-mike/restrict/v2 v2.0.0 h1:OuVBseAejSHyfHMUr15c4Gz3WRCEKuuD8IOR/mOIV/o=
|
||||||
|
github.com/el-mike/restrict/v2 v2.0.0/go.mod h1:ClycXfCKWIZRU1qi2CJIOpHEuonBOj/2GKc+w1lZtrQ=
|
||||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||||
|
@ -61,6 +69,7 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
github.com/golang-migrate/migrate/v4 v4.18.1 h1:JML/k+t4tpHCpQTCAD62Nu43NUFzHY4CV3uAuvHGC+Y=
|
github.com/golang-migrate/migrate/v4 v4.18.1 h1:JML/k+t4tpHCpQTCAD62Nu43NUFzHY4CV3uAuvHGC+Y=
|
||||||
github.com/golang-migrate/migrate/v4 v4.18.1/go.mod h1:HAX6m3sQgcdO81tdjn5exv20+3Kb13cmGli1hrD6hks=
|
github.com/golang-migrate/migrate/v4 v4.18.1/go.mod h1:HAX6m3sQgcdO81tdjn5exv20+3Kb13cmGli1hrD6hks=
|
||||||
|
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
@ -116,6 +125,8 @@ github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNB
|
||||||
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/matoous/go-nanoid v1.5.1 h1:aCjdvTyO9LLnTIi0fgdXhOPPvOHjpXN6Ik9DaNjIct4=
|
||||||
|
github.com/matoous/go-nanoid v1.5.1/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
@ -155,12 +166,16 @@ github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7
|
||||||
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||||
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
@ -181,6 +196,7 @@ go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HY
|
||||||
go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
|
go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
|
||||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
||||||
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
||||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
@ -192,8 +208,11 @@ golang.org/x/image v0.22.0/go.mod h1:9hPFhljd4zZ1GNSIZJ49sqbp45GKK9t6w+iXvGqZUz4
|
||||||
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||||
golang.org/x/mobile v0.0.0-20241108191957-fa514ef75a0f h1:23H/YlmTHfmmvpZ+ajKZL0qLz0+IwFOIqQA0mQbmLeM=
|
golang.org/x/mobile v0.0.0-20241108191957-fa514ef75a0f h1:23H/YlmTHfmmvpZ+ajKZL0qLz0+IwFOIqQA0mQbmLeM=
|
||||||
golang.org/x/mobile v0.0.0-20241108191957-fa514ef75a0f/go.mod h1:UbSUP4uu/C9hw9R2CkojhXlAxvayHjBdU9aRvE+c1To=
|
golang.org/x/mobile v0.0.0-20241108191957-fa514ef75a0f/go.mod h1:UbSUP4uu/C9hw9R2CkojhXlAxvayHjBdU9aRvE+c1To=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
||||||
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
@ -206,6 +225,7 @@ golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
||||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||||
|
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
|
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
|
||||||
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|
46
internal/cache/cache.go
vendored
Normal file
46
internal/cache/cache.go
vendored
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
package cache
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
|
type Cache[K comparable, V any] interface {
|
||||||
|
Get(K) (V, bool)
|
||||||
|
Set(K, V)
|
||||||
|
Delete(K)
|
||||||
|
Clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
type inMem[K comparable, V any] struct {
|
||||||
|
sync.RWMutex
|
||||||
|
m map[K]V
|
||||||
|
}
|
||||||
|
|
||||||
|
func New[K comparable, V any]() *inMem[K, V] {
|
||||||
|
return &inMem[K, V]{
|
||||||
|
m: make(map[K]V),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *inMem[K, V]) Get(key K) (V, bool) {
|
||||||
|
c.RLock()
|
||||||
|
defer c.RUnlock()
|
||||||
|
v, ok := c.m[key]
|
||||||
|
return v, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *inMem[K, V]) Set(key K, val V) {
|
||||||
|
c.Lock()
|
||||||
|
defer c.Unlock()
|
||||||
|
c.m[key] = val
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *inMem[K, V]) Delete(key K) {
|
||||||
|
c.Lock()
|
||||||
|
defer c.Unlock()
|
||||||
|
delete(c.m, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *inMem[K, V]) Clear() {
|
||||||
|
c.Lock()
|
||||||
|
defer c.Unlock()
|
||||||
|
clear(c.m)
|
||||||
|
}
|
33
internal/cache/cache_test.go
vendored
Normal file
33
internal/cache/cache_test.go
vendored
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
package cache_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"dynatron.me/x/stillbox/internal/cache"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCache(t *testing.T) {
|
||||||
|
c := cache.New[int, string]()
|
||||||
|
c.Set(4, "asd")
|
||||||
|
g, ok := c.Get(4)
|
||||||
|
assert.Equal(t, "asd", g)
|
||||||
|
assert.True(t, ok)
|
||||||
|
|
||||||
|
_, ok = c.Get(8)
|
||||||
|
assert.False(t, ok)
|
||||||
|
|
||||||
|
c.Set(7, "fg")
|
||||||
|
|
||||||
|
c.Delete(4)
|
||||||
|
|
||||||
|
g, ok = c.Get(4)
|
||||||
|
assert.False(t, ok)
|
||||||
|
assert.NotEqual(t, "asd", g)
|
||||||
|
|
||||||
|
c.Clear()
|
||||||
|
g, ok = c.Get(7)
|
||||||
|
assert.False(t, ok)
|
||||||
|
assert.NotEqual(t, "fg", g)
|
||||||
|
}
|
|
@ -15,9 +15,9 @@ import (
|
||||||
|
|
||||||
"dynatron.me/x/stillbox/internal/common"
|
"dynatron.me/x/stillbox/internal/common"
|
||||||
"dynatron.me/x/stillbox/internal/forms"
|
"dynatron.me/x/stillbox/internal/forms"
|
||||||
"dynatron.me/x/stillbox/pkg/auth"
|
|
||||||
"dynatron.me/x/stillbox/pkg/calls"
|
"dynatron.me/x/stillbox/pkg/calls"
|
||||||
"dynatron.me/x/stillbox/pkg/sources"
|
"dynatron.me/x/stillbox/pkg/sources"
|
||||||
|
"dynatron.me/x/stillbox/pkg/users"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
@ -62,16 +62,16 @@ func TestMarshal(t *testing.T) {
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
submitter auth.UserID
|
submitter users.UserID
|
||||||
apiKey string
|
apiKey string
|
||||||
call calls.Call
|
call calls.Call
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "base",
|
name: "base",
|
||||||
submitter: auth.UserID(1),
|
submitter: users.UserID(1),
|
||||||
call: calls.Call{
|
call: calls.Call{
|
||||||
ID: uuid.UUID([16]byte{0x52, 0xfd, 0xfc, 0x07, 0x21, 0x82, 0x45, 0x4f, 0x96, 0x3f, 0x5f, 0x0f, 0x9a, 0x62, 0x1d, 0x72}),
|
ID: uuid.UUID([16]byte{0x52, 0xfd, 0xfc, 0x07, 0x21, 0x82, 0x45, 0x4f, 0x96, 0x3f, 0x5f, 0x0f, 0x9a, 0x62, 0x1d, 0x72}),
|
||||||
Submitter: common.PtrTo(auth.UserID(1)),
|
Submitter: common.PtrTo(users.UserID(1)),
|
||||||
System: 197,
|
System: 197,
|
||||||
Talkgroup: 10101,
|
Talkgroup: 10101,
|
||||||
DateTime: time.Date(2024, 11, 10, 23, 33, 02, 0, time.Local),
|
DateTime: time.Date(2024, 11, 10, 23, 33, 02, 0, time.Local),
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"dynatron.me/x/stillbox/pkg/config"
|
"dynatron.me/x/stillbox/pkg/config"
|
||||||
"dynatron.me/x/stillbox/pkg/database"
|
"dynatron.me/x/stillbox/pkg/database"
|
||||||
"dynatron.me/x/stillbox/pkg/notify"
|
"dynatron.me/x/stillbox/pkg/notify"
|
||||||
|
"dynatron.me/x/stillbox/pkg/rbac"
|
||||||
"dynatron.me/x/stillbox/pkg/sinks"
|
"dynatron.me/x/stillbox/pkg/sinks"
|
||||||
"dynatron.me/x/stillbox/pkg/talkgroups"
|
"dynatron.me/x/stillbox/pkg/talkgroups"
|
||||||
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
|
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
|
||||||
|
@ -123,6 +124,8 @@ func New(cfg config.Alerting, tgCache tgstore.Store, opts ...AlertOption) Alerte
|
||||||
|
|
||||||
// Go is the alerting loop. It does not start a goroutine.
|
// Go is the alerting loop. It does not start a goroutine.
|
||||||
func (as *alerter) Go(ctx context.Context) {
|
func (as *alerter) Go(ctx context.Context) {
|
||||||
|
ctx = rbac.CtxWithSubject(ctx, &rbac.SystemServiceSubject{Name: "alerter"})
|
||||||
|
|
||||||
err := as.startBackfill(ctx)
|
err := as.startBackfill(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("backfill")
|
log.Error().Err(err).Msg("backfill")
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"dynatron.me/x/stillbox/internal/jsontypes"
|
"dynatron.me/x/stillbox/internal/jsontypes"
|
||||||
"dynatron.me/x/stillbox/internal/trending"
|
"dynatron.me/x/stillbox/internal/trending"
|
||||||
"dynatron.me/x/stillbox/pkg/config"
|
"dynatron.me/x/stillbox/pkg/config"
|
||||||
|
"dynatron.me/x/stillbox/pkg/database"
|
||||||
"dynatron.me/x/stillbox/pkg/talkgroups"
|
"dynatron.me/x/stillbox/pkg/talkgroups"
|
||||||
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
|
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
|
||||||
|
|
||||||
|
@ -59,8 +60,9 @@ func (s *Simulation) stepClock(t time.Time) {
|
||||||
|
|
||||||
// Simulate begins the simulation using the DB handle from ctx. It returns final scores.
|
// Simulate begins the simulation using the DB handle from ctx. It returns final scores.
|
||||||
func (s *Simulation) Simulate(ctx context.Context) (trending.Scores[talkgroups.ID], error) {
|
func (s *Simulation) Simulate(ctx context.Context) (trending.Scores[talkgroups.ID], error) {
|
||||||
|
db := database.FromCtx(ctx)
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
tgc := tgstore.NewCache()
|
tgc := tgstore.NewCache(db)
|
||||||
|
|
||||||
s.Enable = true
|
s.Enable = true
|
||||||
s.alerter = New(s.Alerting, tgc, WithClock(&s.clock)).(*alerter)
|
s.alerter = New(s.Alerting, tgc, WithClock(&s.clock)).(*alerter)
|
||||||
|
|
|
@ -7,28 +7,28 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"dynatron.me/x/stillbox/pkg/database"
|
"dynatron.me/x/stillbox/pkg/database"
|
||||||
|
"dynatron.me/x/stillbox/pkg/rbac"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type apiKeyAuth interface {
|
type apiKeyAuth interface {
|
||||||
// CheckAPIKey validates the provided key and returns the API owner's UserID.
|
// CheckAPIKey validates the provided key and returns the API owner's users.UserID.
|
||||||
// An error is returned if validation fails for any reason.
|
// An error is returned if validation fails for any reason.
|
||||||
CheckAPIKey(ctx context.Context, key string) (*UserID, error)
|
CheckAPIKey(ctx context.Context, key string) (rbac.Subject, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Auth) CheckAPIKey(ctx context.Context, key string) (*UserID, error) {
|
func (a *Auth) CheckAPIKey(ctx context.Context, key string) (rbac.Subject, error) {
|
||||||
keyUuid, err := uuid.Parse(key)
|
keyUuid, err := uuid.Parse(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Str("apikey", key).Msg("cannot parse key")
|
log.Error().Str("apikey", key).Msg("cannot parse key")
|
||||||
return nil, ErrBadRequest
|
return nil, ErrBadRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
db := database.FromCtx(ctx)
|
|
||||||
hash := sha256.Sum256([]byte(keyUuid.String()))
|
hash := sha256.Sum256([]byte(keyUuid.String()))
|
||||||
b64hash := base64.StdEncoding.EncodeToString(hash[:])
|
b64hash := base64.StdEncoding.EncodeToString(hash[:])
|
||||||
apik, err := db.GetAPIKey(ctx, b64hash)
|
apik, err := a.ust.GetAPIKey(ctx, b64hash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if database.IsNoRows(err) {
|
if database.IsNoRows(err) {
|
||||||
log.Error().Str("apikey", keyUuid.String()).Msg("no such key")
|
log.Error().Str("apikey", keyUuid.String()).Msg("no such key")
|
||||||
|
@ -44,7 +44,5 @@ func (a *Auth) CheckAPIKey(ctx context.Context, key string) (*UserID, error) {
|
||||||
return nil, ErrUnauthorized
|
return nil, ErrUnauthorized
|
||||||
}
|
}
|
||||||
|
|
||||||
owner := UserID(apik.Owner)
|
return a.ust.GetUser(ctx, apik.Username)
|
||||||
|
|
||||||
return &owner, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,23 +8,13 @@ import (
|
||||||
_ "embed"
|
_ "embed"
|
||||||
|
|
||||||
"dynatron.me/x/stillbox/pkg/config"
|
"dynatron.me/x/stillbox/pkg/config"
|
||||||
|
"dynatron.me/x/stillbox/pkg/rbac"
|
||||||
|
"dynatron.me/x/stillbox/pkg/users"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/go-chi/httprate"
|
"github.com/go-chi/httprate"
|
||||||
"github.com/go-chi/jwtauth/v5"
|
"github.com/go-chi/jwtauth/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserID int
|
|
||||||
|
|
||||||
func (u *UserID) Int32Ptr() *int32 {
|
|
||||||
if u == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
i := int32(*u)
|
|
||||||
|
|
||||||
return &i
|
|
||||||
}
|
|
||||||
|
|
||||||
// Authenticator performs API key and user JWT authentication.
|
// Authenticator performs API key and user JWT authentication.
|
||||||
type Authenticator interface {
|
type Authenticator interface {
|
||||||
jwtAuth
|
jwtAuth
|
||||||
|
@ -34,14 +24,16 @@ type Authenticator interface {
|
||||||
type Auth struct {
|
type Auth struct {
|
||||||
rl *httprate.RateLimiter
|
rl *httprate.RateLimiter
|
||||||
jwt *jwtauth.JWTAuth
|
jwt *jwtauth.JWTAuth
|
||||||
|
ust users.Store
|
||||||
cfg config.Auth
|
cfg config.Auth
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAuthenticator creates a new Authenticator with the provided config.
|
// NewAuthenticator creates a new Authenticator with the provided config.
|
||||||
func NewAuthenticator(cfg config.Auth) *Auth {
|
func NewAuthenticator(cfg config.Auth, ust users.Store) *Auth {
|
||||||
a := &Auth{
|
a := &Auth{
|
||||||
rl: httprate.NewRateLimiter(5, time.Minute),
|
rl: httprate.NewRateLimiter(5, time.Minute),
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
|
ust: ust,
|
||||||
}
|
}
|
||||||
a.initJWT()
|
a.initJWT()
|
||||||
|
|
||||||
|
@ -63,7 +55,7 @@ var (
|
||||||
// ErrorResponse writes the error and appropriate HTTP response code.
|
// ErrorResponse writes the error and appropriate HTTP response code.
|
||||||
func ErrorResponse(w http.ResponseWriter, err error) {
|
func ErrorResponse(w http.ResponseWriter, err error) {
|
||||||
switch err {
|
switch err {
|
||||||
case ErrLoginFailed, ErrUnauthorized:
|
case ErrLoginFailed, ErrUnauthorized, rbac.ErrBadSubject:
|
||||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||||
case ErrBadRequest:
|
case ErrBadRequest:
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
|
|
@ -4,17 +4,19 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
"dynatron.me/x/stillbox/pkg/database"
|
"dynatron.me/x/stillbox/pkg/database"
|
||||||
|
"dynatron.me/x/stillbox/pkg/rbac"
|
||||||
|
"dynatron.me/x/stillbox/pkg/users"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/go-chi/jwtauth/v5"
|
"github.com/go-chi/jwtauth/v5"
|
||||||
"github.com/go-chi/render"
|
"github.com/go-chi/render"
|
||||||
|
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -44,21 +46,16 @@ type jwtAuth interface {
|
||||||
|
|
||||||
type claims map[string]interface{}
|
type claims map[string]interface{}
|
||||||
|
|
||||||
func UIDFrom(ctx context.Context) *int32 {
|
// UsernameFrom gets the username (just the subject from token) from ctx.
|
||||||
|
func UsernameFrom(ctx context.Context) *string {
|
||||||
tok, _, err := jwtauth.FromContext(ctx)
|
tok, _, err := jwtauth.FromContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
uidStr := tok.Subject()
|
username := tok.Subject()
|
||||||
uidInt, err := strconv.Atoi(uidStr)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
uid := int32(uidInt)
|
return &username
|
||||||
|
|
||||||
return &uid
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Auth) Authenticated(r *http.Request) (claims, bool) {
|
func (a *Auth) Authenticated(r *http.Request) (claims, bool) {
|
||||||
|
@ -88,7 +85,38 @@ func TokenFromCookie(r *http.Request) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Auth) AuthMiddleware() func(http.Handler) http.Handler {
|
func (a *Auth) AuthMiddleware() func(http.Handler) http.Handler {
|
||||||
return jwtauth.Authenticator(a.jwt)
|
return func(next http.Handler) http.Handler {
|
||||||
|
hfn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
token, _, err := jwtauth.FromContext(r.Context())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if token != nil && jwt.Validate(token, a.jwt.ValidateOptions()...) == nil {
|
||||||
|
ctx := r.Context()
|
||||||
|
username := token.Subject()
|
||||||
|
|
||||||
|
sub, err := users.FromCtx(ctx).GetUser(ctx, username)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = rbac.CtxWithSubject(ctx, sub)
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token is authenticated, pass it through
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
return http.HandlerFunc(hfn)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Auth) initJWT() {
|
func (a *Auth) initJWT() {
|
||||||
|
@ -124,12 +152,12 @@ func (a *Auth) Login(ctx context.Context, username, password string) (token stri
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return a.newToken(found.ID), nil
|
return a.newToken(found.Username), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Auth) newToken(uid int) string {
|
func (a *Auth) newToken(username string) string {
|
||||||
claims := claims{
|
claims := claims{
|
||||||
"sub": strconv.Itoa(int(uid)),
|
"sub": username,
|
||||||
}
|
}
|
||||||
jwtauth.SetExpiryIn(claims, time.Hour*24*30) // one month
|
jwtauth.SetExpiryIn(claims, time.Hour*24*30) // one month
|
||||||
_, tokenString, err := a.jwt.Encode(claims)
|
_, tokenString, err := a.jwt.Encode(claims)
|
||||||
|
@ -161,19 +189,14 @@ func (a *Auth) routeRefresh(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Error(w, "Invalid token", http.StatusBadRequest)
|
http.Error(w, "Invalid token", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
existingSubjectUID := jwToken.Subject()
|
|
||||||
if existingSubjectUID == "" {
|
existingSubjectUsername := jwToken.Subject()
|
||||||
|
if existingSubjectUsername == "" {
|
||||||
http.Error(w, "Invalid token", http.StatusBadRequest)
|
http.Error(w, "Invalid token", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
uid, err := strconv.Atoi(existingSubjectUID)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Str("sub", existingSubjectUID).Err(err).Msg("atoi uid for token refresh")
|
|
||||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tok := a.newToken(uid)
|
tok := a.newToken(existingSubjectUsername)
|
||||||
|
|
||||||
cookie := &http.Cookie{
|
cookie := &http.Cookie{
|
||||||
Name: CookieName,
|
Name: CookieName,
|
||||||
|
|
|
@ -7,9 +7,10 @@ import (
|
||||||
|
|
||||||
"dynatron.me/x/stillbox/internal/audio"
|
"dynatron.me/x/stillbox/internal/audio"
|
||||||
"dynatron.me/x/stillbox/internal/jsontypes"
|
"dynatron.me/x/stillbox/internal/jsontypes"
|
||||||
"dynatron.me/x/stillbox/pkg/auth"
|
|
||||||
"dynatron.me/x/stillbox/pkg/pb"
|
"dynatron.me/x/stillbox/pkg/pb"
|
||||||
|
"dynatron.me/x/stillbox/pkg/rbac"
|
||||||
"dynatron.me/x/stillbox/pkg/talkgroups"
|
"dynatron.me/x/stillbox/pkg/talkgroups"
|
||||||
|
"dynatron.me/x/stillbox/pkg/users"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
@ -52,27 +53,31 @@ type CallAudio struct {
|
||||||
// further transformation. relayOut exists for compatibility with http
|
// further transformation. relayOut exists for compatibility with http
|
||||||
// source CallUploadRequest as used in the relay sink.
|
// source CallUploadRequest as used in the relay sink.
|
||||||
type Call struct {
|
type Call struct {
|
||||||
ID uuid.UUID `json:"id" relayOut:"id"`
|
ID uuid.UUID `json:"id" relayOut:"id"`
|
||||||
Audio []byte `json:"audio,omitempty" relayOut:"audio,omitempty" filenameField:"AudioName"`
|
Audio []byte `json:"audio,omitempty" relayOut:"audio,omitempty" filenameField:"AudioName"`
|
||||||
AudioName string `json:"audioName,omitempty" relayOut:"audioName,omitempty"`
|
AudioName string `json:"audioName,omitempty" relayOut:"audioName,omitempty"`
|
||||||
AudioType string `json:"audioType,omitempty" relayOut:"audioType,omitempty"`
|
AudioType string `json:"audioType,omitempty" relayOut:"audioType,omitempty"`
|
||||||
Duration CallDuration `json:"duration,omitempty" relayOut:"duration,omitempty"`
|
Duration CallDuration `json:"duration,omitempty" relayOut:"duration,omitempty"`
|
||||||
DateTime time.Time `json:"call_date,omitempty" relayOut:"dateTime,omitempty"`
|
DateTime time.Time `json:"call_date,omitempty" relayOut:"dateTime,omitempty"`
|
||||||
Frequencies []int `json:"frequencies,omitempty" relayOut:"frequencies,omitempty"`
|
Frequencies []int `json:"frequencies,omitempty" relayOut:"frequencies,omitempty"`
|
||||||
Frequency int `json:"frequency,omitempty" relayOut:"frequency,omitempty"`
|
Frequency int `json:"frequency,omitempty" relayOut:"frequency,omitempty"`
|
||||||
Patches []int `json:"patches,omitempty" relayOut:"patches,omitempty"`
|
Patches []int `json:"patches,omitempty" relayOut:"patches,omitempty"`
|
||||||
Source int `json:"source,omitempty" relayOut:"source,omitempty"`
|
Source int `json:"source,omitempty" relayOut:"source,omitempty"`
|
||||||
System int `json:"system_id,omitempty" relayOut:"system,omitempty"`
|
System int `json:"system_id,omitempty" relayOut:"system,omitempty"`
|
||||||
Submitter *auth.UserID `json:"submitter,omitempty" relayOut:"submitter,omitempty"`
|
Submitter *users.UserID `json:"submitter,omitempty" relayOut:"submitter,omitempty"`
|
||||||
SystemLabel string `json:"system_name,omitempty" relayOut:"systemLabel,omitempty"`
|
SystemLabel string `json:"system_name,omitempty" relayOut:"systemLabel,omitempty"`
|
||||||
Talkgroup int `json:"tgid,omitempty" relayOut:"talkgroup,omitempty"`
|
Talkgroup int `json:"tgid,omitempty" relayOut:"talkgroup,omitempty"`
|
||||||
TalkgroupGroup *string `json:"talkgroupGroup,omitempty" relayOut:"talkgroupGroup,omitempty"`
|
TalkgroupGroup *string `json:"talkgroupGroup,omitempty" relayOut:"talkgroupGroup,omitempty"`
|
||||||
TalkgroupLabel *string `json:"talkgroupLabel,omitempty" relayOut:"talkgroupLabel,omitempty"`
|
TalkgroupLabel *string `json:"talkgroupLabel,omitempty" relayOut:"talkgroupLabel,omitempty"`
|
||||||
TGAlphaTag *string `json:"tg_name,omitempty" relayOut:"talkgroupTag,omitempty"`
|
TGAlphaTag *string `json:"tg_name,omitempty" relayOut:"talkgroupTag,omitempty"`
|
||||||
|
|
||||||
shouldStore bool `json:"-"`
|
shouldStore bool `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Call) GetResourceName() string {
|
||||||
|
return rbac.ResourceCall
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Call) String() string {
|
func (c *Call) String() string {
|
||||||
return fmt.Sprintf("%s to %d from %d", c.AudioName, c.Talkgroup, c.Source)
|
return fmt.Sprintf("%s to %d from %d", c.AudioName, c.Talkgroup, c.Source)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,9 @@ import (
|
||||||
|
|
||||||
"dynatron.me/x/stillbox/pkg/calls"
|
"dynatron.me/x/stillbox/pkg/calls"
|
||||||
"dynatron.me/x/stillbox/pkg/database"
|
"dynatron.me/x/stillbox/pkg/database"
|
||||||
|
"dynatron.me/x/stillbox/pkg/rbac"
|
||||||
|
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
|
||||||
|
"dynatron.me/x/stillbox/pkg/users"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
|
@ -16,6 +19,12 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Store interface {
|
type Store interface {
|
||||||
|
// AddCall adds a call to the database.
|
||||||
|
AddCall(ctx context.Context, call *calls.Call) error
|
||||||
|
|
||||||
|
// DeleteCall deletes a call.
|
||||||
|
Delete(ctx context.Context, id uuid.UUID) error
|
||||||
|
|
||||||
// CallAudio returns a CallAudio struct
|
// CallAudio returns a CallAudio struct
|
||||||
CallAudio(ctx context.Context, id uuid.UUID) (*calls.CallAudio, error)
|
CallAudio(ctx context.Context, id uuid.UUID) (*calls.CallAudio, error)
|
||||||
|
|
||||||
|
@ -24,10 +33,13 @@ type Store interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
type store struct {
|
type store struct {
|
||||||
|
db database.Store
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStore() *store {
|
func NewStore(db database.Store) *store {
|
||||||
return new(store)
|
return &store{
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type storeCtxKey string
|
type storeCtxKey string
|
||||||
|
@ -41,13 +53,77 @@ func CtxWithStore(ctx context.Context, s Store) context.Context {
|
||||||
func FromCtx(ctx context.Context) Store {
|
func FromCtx(ctx context.Context) Store {
|
||||||
s, ok := ctx.Value(StoreCtxKey).(Store)
|
s, ok := ctx.Value(StoreCtxKey).(Store)
|
||||||
if !ok {
|
if !ok {
|
||||||
return NewStore()
|
panic("no call store in context")
|
||||||
}
|
}
|
||||||
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toAddCallParams(call *calls.Call) database.AddCallParams {
|
||||||
|
return database.AddCallParams{
|
||||||
|
ID: call.ID,
|
||||||
|
Submitter: call.Submitter.Int32Ptr(),
|
||||||
|
System: call.System,
|
||||||
|
Talkgroup: call.Talkgroup,
|
||||||
|
CallDate: pgtype.Timestamptz{Time: call.DateTime, Valid: true},
|
||||||
|
AudioName: common.NilIfZero(call.AudioName),
|
||||||
|
AudioBlob: call.Audio,
|
||||||
|
AudioType: common.NilIfZero(call.AudioType),
|
||||||
|
Duration: call.Duration.MsInt32Ptr(),
|
||||||
|
Frequency: call.Frequency,
|
||||||
|
Frequencies: call.Frequencies,
|
||||||
|
Patches: call.Patches,
|
||||||
|
TGLabel: call.TalkgroupLabel,
|
||||||
|
TGAlphaTag: call.TGAlphaTag,
|
||||||
|
TGGroup: call.TalkgroupGroup,
|
||||||
|
Source: call.Source,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *store) AddCall(ctx context.Context, call *calls.Call) error {
|
||||||
|
_, err := rbac.Check(ctx, call, rbac.WithActions(rbac.ActionCreate))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
params := toAddCallParams(call)
|
||||||
|
db := database.FromCtx(ctx)
|
||||||
|
tgs := tgstore.FromCtx(ctx)
|
||||||
|
|
||||||
|
err = db.InTx(ctx, func(tx database.Store) error {
|
||||||
|
err := tx.AddCall(ctx, params)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("add call: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}, pgx.TxOptions{})
|
||||||
|
|
||||||
|
if err != nil && database.IsTGConstraintViolation(err) {
|
||||||
|
return db.InTx(ctx, func(tx database.Store) error {
|
||||||
|
_, err := tgs.LearnTG(ctx, call)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("learn tg: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.AddCall(ctx, params)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("learn tg retry: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}, pgx.TxOptions{})
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *store) CallAudio(ctx context.Context, id uuid.UUID) (*calls.CallAudio, error) {
|
func (s *store) CallAudio(ctx context.Context, id uuid.UUID) (*calls.CallAudio, error) {
|
||||||
|
_, err := rbac.Check(ctx, rbac.UseResource(rbac.ResourceCall), rbac.WithActions(rbac.ActionRead))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
db := database.FromCtx(ctx)
|
db := database.FromCtx(ctx)
|
||||||
|
|
||||||
dbCall, err := db.GetCallAudioByID(ctx, id)
|
dbCall, err := db.GetCallAudioByID(ctx, id)
|
||||||
|
@ -76,6 +152,11 @@ type CallsParams struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *store) Calls(ctx context.Context, p CallsParams) (rows []database.ListCallsPRow, totalCount int, err error) {
|
func (s *store) Calls(ctx context.Context, p CallsParams) (rows []database.ListCallsPRow, totalCount int, err error) {
|
||||||
|
_, err = rbac.Check(ctx, rbac.UseResource(rbac.ResourceCall), rbac.WithActions(rbac.ActionRead))
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
db := database.FromCtx(ctx)
|
db := database.FromCtx(ctx)
|
||||||
|
|
||||||
offset, perPage := p.Pagination.OffsetPerPage(100)
|
offset, perPage := p.Pagination.OffsetPerPage(100)
|
||||||
|
@ -127,3 +208,28 @@ func (s *store) Calls(ctx context.Context, p CallsParams) (rows []database.ListC
|
||||||
|
|
||||||
return rows, int(count), err
|
return rows, int(count), err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *store) Delete(ctx context.Context, id uuid.UUID) error {
|
||||||
|
callOwn, err := s.getCallOwner(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = rbac.Check(ctx, &callOwn, rbac.WithActions(rbac.ActionDelete))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return database.FromCtx(ctx).DeleteCall(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *store) getCallOwner(ctx context.Context, id uuid.UUID) (calls.Call, error) {
|
||||||
|
subInt, err := database.FromCtx(ctx).GetCallSubmitter(ctx, id)
|
||||||
|
|
||||||
|
var sub *users.UserID
|
||||||
|
|
||||||
|
if subInt != nil {
|
||||||
|
sub = common.PtrTo(users.UserID(*subInt))
|
||||||
|
}
|
||||||
|
return calls.Call{ID: id, Submitter: sub}, err
|
||||||
|
}
|
||||||
|
|
|
@ -155,6 +155,15 @@ func (q *Queries) CleanupSweptCalls(ctx context.Context, rangeStart pgtype.Times
|
||||||
return result.RowsAffected(), nil
|
return result.RowsAffected(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deleteCall = `-- name: DeleteCall :exec
|
||||||
|
DELETE FROM calls WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteCall(ctx context.Context, id uuid.UUID) error {
|
||||||
|
_, err := q.db.Exec(ctx, deleteCall, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
const getCallAudioByID = `-- name: GetCallAudioByID :one
|
const getCallAudioByID = `-- name: GetCallAudioByID :one
|
||||||
SELECT
|
SELECT
|
||||||
c.call_date,
|
c.call_date,
|
||||||
|
@ -192,6 +201,17 @@ func (q *Queries) GetCallAudioByID(ctx context.Context, id uuid.UUID) (GetCallAu
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getCallSubmitter = `-- name: GetCallSubmitter :one
|
||||||
|
SELECT submitter FROM calls WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetCallSubmitter(ctx context.Context, id uuid.UUID) (*int32, error) {
|
||||||
|
row := q.db.QueryRow(ctx, getCallSubmitter, id)
|
||||||
|
var submitter *int32
|
||||||
|
err := row.Scan(&submitter)
|
||||||
|
return submitter, err
|
||||||
|
}
|
||||||
|
|
||||||
const getDatabaseSize = `-- name: GetDatabaseSize :one
|
const getDatabaseSize = `-- name: GetDatabaseSize :one
|
||||||
SELECT pg_size_pretty(pg_database_size(current_database()))
|
SELECT pg_size_pretty(pg_database_size(current_database()))
|
||||||
`
|
`
|
||||||
|
|
|
@ -44,6 +44,7 @@ const createIncident = `-- name: CreateIncident :one
|
||||||
INSERT INTO incidents (
|
INSERT INTO incidents (
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
|
owner,
|
||||||
description,
|
description,
|
||||||
start_time,
|
start_time,
|
||||||
end_time,
|
end_time,
|
||||||
|
@ -56,14 +57,16 @@ INSERT INTO incidents (
|
||||||
$4,
|
$4,
|
||||||
$5,
|
$5,
|
||||||
$6,
|
$6,
|
||||||
$7
|
$7,
|
||||||
|
$8
|
||||||
)
|
)
|
||||||
RETURNING id, name, description, start_time, end_time, location, metadata
|
RETURNING id, name, owner, description, start_time, end_time, location, metadata
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreateIncidentParams struct {
|
type CreateIncidentParams struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
Owner int `json:"owner"`
|
||||||
Description *string `json:"description"`
|
Description *string `json:"description"`
|
||||||
StartTime pgtype.Timestamptz `json:"start_time"`
|
StartTime pgtype.Timestamptz `json:"start_time"`
|
||||||
EndTime pgtype.Timestamptz `json:"end_time"`
|
EndTime pgtype.Timestamptz `json:"end_time"`
|
||||||
|
@ -75,6 +78,7 @@ func (q *Queries) CreateIncident(ctx context.Context, arg CreateIncidentParams)
|
||||||
row := q.db.QueryRow(ctx, createIncident,
|
row := q.db.QueryRow(ctx, createIncident,
|
||||||
arg.ID,
|
arg.ID,
|
||||||
arg.Name,
|
arg.Name,
|
||||||
|
arg.Owner,
|
||||||
arg.Description,
|
arg.Description,
|
||||||
arg.StartTime,
|
arg.StartTime,
|
||||||
arg.EndTime,
|
arg.EndTime,
|
||||||
|
@ -85,6 +89,7 @@ func (q *Queries) CreateIncident(ctx context.Context, arg CreateIncidentParams)
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.Name,
|
&i.Name,
|
||||||
|
&i.Owner,
|
||||||
&i.Description,
|
&i.Description,
|
||||||
&i.StartTime,
|
&i.StartTime,
|
||||||
&i.EndTime,
|
&i.EndTime,
|
||||||
|
@ -107,6 +112,7 @@ const getIncident = `-- name: GetIncident :one
|
||||||
SELECT
|
SELECT
|
||||||
i.id,
|
i.id,
|
||||||
i.name,
|
i.name,
|
||||||
|
i.owner,
|
||||||
i.description,
|
i.description,
|
||||||
i.start_time,
|
i.start_time,
|
||||||
i.end_time,
|
i.end_time,
|
||||||
|
@ -122,6 +128,7 @@ func (q *Queries) GetIncident(ctx context.Context, id uuid.UUID) (Incident, erro
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.Name,
|
&i.Name,
|
||||||
|
&i.Owner,
|
||||||
&i.Description,
|
&i.Description,
|
||||||
&i.StartTime,
|
&i.StartTime,
|
||||||
&i.EndTime,
|
&i.EndTime,
|
||||||
|
@ -237,6 +244,17 @@ func (q *Queries) GetIncidentCalls(ctx context.Context, id uuid.UUID) ([]GetInci
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getIncidentOwner = `-- name: GetIncidentOwner :one
|
||||||
|
SELECT owner FROM incidents WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetIncidentOwner(ctx context.Context, id uuid.UUID) (int, error) {
|
||||||
|
row := q.db.QueryRow(ctx, getIncidentOwner, id)
|
||||||
|
var owner int
|
||||||
|
err := row.Scan(&owner)
|
||||||
|
return owner, err
|
||||||
|
}
|
||||||
|
|
||||||
const listIncidentsCount = `-- name: ListIncidentsCount :one
|
const listIncidentsCount = `-- name: ListIncidentsCount :one
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
FROM incidents i
|
FROM incidents i
|
||||||
|
@ -262,6 +280,7 @@ const listIncidentsP = `-- name: ListIncidentsP :many
|
||||||
SELECT
|
SELECT
|
||||||
i.id,
|
i.id,
|
||||||
i.name,
|
i.name,
|
||||||
|
i.owner,
|
||||||
i.description,
|
i.description,
|
||||||
i.start_time,
|
i.start_time,
|
||||||
i.end_time,
|
i.end_time,
|
||||||
|
@ -299,6 +318,7 @@ type ListIncidentsPParams struct {
|
||||||
type ListIncidentsPRow struct {
|
type ListIncidentsPRow struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
Owner int `json:"owner"`
|
||||||
Description *string `json:"description"`
|
Description *string `json:"description"`
|
||||||
StartTime pgtype.Timestamptz `json:"start_time"`
|
StartTime pgtype.Timestamptz `json:"start_time"`
|
||||||
EndTime pgtype.Timestamptz `json:"end_time"`
|
EndTime pgtype.Timestamptz `json:"end_time"`
|
||||||
|
@ -326,6 +346,7 @@ func (q *Queries) ListIncidentsP(ctx context.Context, arg ListIncidentsPParams)
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.Name,
|
&i.Name,
|
||||||
|
&i.Owner,
|
||||||
&i.Description,
|
&i.Description,
|
||||||
&i.StartTime,
|
&i.StartTime,
|
||||||
&i.EndTime,
|
&i.EndTime,
|
||||||
|
@ -375,7 +396,7 @@ SET
|
||||||
metadata = COALESCE($6, metadata)
|
metadata = COALESCE($6, metadata)
|
||||||
WHERE
|
WHERE
|
||||||
id = $7
|
id = $7
|
||||||
RETURNING id, name, description, start_time, end_time, location, metadata
|
RETURNING id, name, owner, description, start_time, end_time, location, metadata
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateIncidentParams struct {
|
type UpdateIncidentParams struct {
|
||||||
|
@ -402,6 +423,7 @@ func (q *Queries) UpdateIncident(ctx context.Context, arg UpdateIncidentParams)
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.Name,
|
&i.Name,
|
||||||
|
&i.Owner,
|
||||||
&i.Description,
|
&i.Description,
|
||||||
&i.StartTime,
|
&i.StartTime,
|
||||||
&i.EndTime,
|
&i.EndTime,
|
||||||
|
|
|
@ -502,6 +502,53 @@ func (_c *Store_CreatePartition_Call) RunAndReturn(run func(context.Context, str
|
||||||
return _c
|
return _c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateShare provides a mock function with given fields: ctx, arg
|
||||||
|
func (_m *Store) CreateShare(ctx context.Context, arg database.CreateShareParams) error {
|
||||||
|
ret := _m.Called(ctx, arg)
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for CreateShare")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, database.CreateShareParams) error); ok {
|
||||||
|
r0 = rf(ctx, arg)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store_CreateShare_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateShare'
|
||||||
|
type Store_CreateShare_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateShare is a helper method to define mock.On call
|
||||||
|
// - ctx context.Context
|
||||||
|
// - arg database.CreateShareParams
|
||||||
|
func (_e *Store_Expecter) CreateShare(ctx interface{}, arg interface{}) *Store_CreateShare_Call {
|
||||||
|
return &Store_CreateShare_Call{Call: _e.mock.On("CreateShare", ctx, arg)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *Store_CreateShare_Call) Run(run func(ctx context.Context, arg database.CreateShareParams)) *Store_CreateShare_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
run(args[0].(context.Context), args[1].(database.CreateShareParams))
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *Store_CreateShare_Call) Return(_a0 error) *Store_CreateShare_Call {
|
||||||
|
_c.Call.Return(_a0)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *Store_CreateShare_Call) RunAndReturn(run func(context.Context, database.CreateShareParams) error) *Store_CreateShare_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
// CreateSystem provides a mock function with given fields: ctx, iD, name
|
// CreateSystem provides a mock function with given fields: ctx, iD, name
|
||||||
func (_m *Store) CreateSystem(ctx context.Context, iD int, name string) error {
|
func (_m *Store) CreateSystem(ctx context.Context, iD int, name string) error {
|
||||||
ret := _m.Called(ctx, iD, name)
|
ret := _m.Called(ctx, iD, name)
|
||||||
|
@ -748,6 +795,53 @@ func (_c *Store_DeleteAPIKey_Call) RunAndReturn(run func(context.Context, string
|
||||||
return _c
|
return _c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteCall provides a mock function with given fields: ctx, id
|
||||||
|
func (_m *Store) DeleteCall(ctx context.Context, id uuid.UUID) error {
|
||||||
|
ret := _m.Called(ctx, id)
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for DeleteCall")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) error); ok {
|
||||||
|
r0 = rf(ctx, id)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store_DeleteCall_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteCall'
|
||||||
|
type Store_DeleteCall_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteCall is a helper method to define mock.On call
|
||||||
|
// - ctx context.Context
|
||||||
|
// - id uuid.UUID
|
||||||
|
func (_e *Store_Expecter) DeleteCall(ctx interface{}, id interface{}) *Store_DeleteCall_Call {
|
||||||
|
return &Store_DeleteCall_Call{Call: _e.mock.On("DeleteCall", ctx, id)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *Store_DeleteCall_Call) Run(run func(ctx context.Context, id uuid.UUID)) *Store_DeleteCall_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
run(args[0].(context.Context), args[1].(uuid.UUID))
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *Store_DeleteCall_Call) Return(_a0 error) *Store_DeleteCall_Call {
|
||||||
|
_c.Call.Return(_a0)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *Store_DeleteCall_Call) RunAndReturn(run func(context.Context, uuid.UUID) error) *Store_DeleteCall_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
// DeleteIncident provides a mock function with given fields: ctx, id
|
// DeleteIncident provides a mock function with given fields: ctx, id
|
||||||
func (_m *Store) DeleteIncident(ctx context.Context, id uuid.UUID) error {
|
func (_m *Store) DeleteIncident(ctx context.Context, id uuid.UUID) error {
|
||||||
ret := _m.Called(ctx, id)
|
ret := _m.Called(ctx, id)
|
||||||
|
@ -795,6 +889,53 @@ func (_c *Store_DeleteIncident_Call) RunAndReturn(run func(context.Context, uuid
|
||||||
return _c
|
return _c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteShare provides a mock function with given fields: ctx, id
|
||||||
|
func (_m *Store) DeleteShare(ctx context.Context, id string) error {
|
||||||
|
ret := _m.Called(ctx, id)
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for DeleteShare")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
|
||||||
|
r0 = rf(ctx, id)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store_DeleteShare_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteShare'
|
||||||
|
type Store_DeleteShare_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteShare is a helper method to define mock.On call
|
||||||
|
// - ctx context.Context
|
||||||
|
// - id string
|
||||||
|
func (_e *Store_Expecter) DeleteShare(ctx interface{}, id interface{}) *Store_DeleteShare_Call {
|
||||||
|
return &Store_DeleteShare_Call{Call: _e.mock.On("DeleteShare", ctx, id)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *Store_DeleteShare_Call) Run(run func(ctx context.Context, id string)) *Store_DeleteShare_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
run(args[0].(context.Context), args[1].(string))
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *Store_DeleteShare_Call) Return(_a0 error) *Store_DeleteShare_Call {
|
||||||
|
_c.Call.Return(_a0)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *Store_DeleteShare_Call) RunAndReturn(run func(context.Context, string) error) *Store_DeleteShare_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
// DeleteSystem provides a mock function with given fields: ctx, id
|
// DeleteSystem provides a mock function with given fields: ctx, id
|
||||||
func (_m *Store) DeleteSystem(ctx context.Context, id int) error {
|
func (_m *Store) DeleteSystem(ctx context.Context, id int) error {
|
||||||
ret := _m.Called(ctx, id)
|
ret := _m.Called(ctx, id)
|
||||||
|
@ -1033,22 +1174,22 @@ func (_c *Store_DropPartition_Call) RunAndReturn(run func(context.Context, strin
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAPIKey provides a mock function with given fields: ctx, apiKey
|
// GetAPIKey provides a mock function with given fields: ctx, apiKey
|
||||||
func (_m *Store) GetAPIKey(ctx context.Context, apiKey string) (database.ApiKey, error) {
|
func (_m *Store) GetAPIKey(ctx context.Context, apiKey string) (database.GetAPIKeyRow, error) {
|
||||||
ret := _m.Called(ctx, apiKey)
|
ret := _m.Called(ctx, apiKey)
|
||||||
|
|
||||||
if len(ret) == 0 {
|
if len(ret) == 0 {
|
||||||
panic("no return value specified for GetAPIKey")
|
panic("no return value specified for GetAPIKey")
|
||||||
}
|
}
|
||||||
|
|
||||||
var r0 database.ApiKey
|
var r0 database.GetAPIKeyRow
|
||||||
var r1 error
|
var r1 error
|
||||||
if rf, ok := ret.Get(0).(func(context.Context, string) (database.ApiKey, error)); ok {
|
if rf, ok := ret.Get(0).(func(context.Context, string) (database.GetAPIKeyRow, error)); ok {
|
||||||
return rf(ctx, apiKey)
|
return rf(ctx, apiKey)
|
||||||
}
|
}
|
||||||
if rf, ok := ret.Get(0).(func(context.Context, string) database.ApiKey); ok {
|
if rf, ok := ret.Get(0).(func(context.Context, string) database.GetAPIKeyRow); ok {
|
||||||
r0 = rf(ctx, apiKey)
|
r0 = rf(ctx, apiKey)
|
||||||
} else {
|
} else {
|
||||||
r0 = ret.Get(0).(database.ApiKey)
|
r0 = ret.Get(0).(database.GetAPIKeyRow)
|
||||||
}
|
}
|
||||||
|
|
||||||
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||||
|
@ -1079,12 +1220,12 @@ func (_c *Store_GetAPIKey_Call) Run(run func(ctx context.Context, apiKey string)
|
||||||
return _c
|
return _c
|
||||||
}
|
}
|
||||||
|
|
||||||
func (_c *Store_GetAPIKey_Call) Return(_a0 database.ApiKey, _a1 error) *Store_GetAPIKey_Call {
|
func (_c *Store_GetAPIKey_Call) Return(_a0 database.GetAPIKeyRow, _a1 error) *Store_GetAPIKey_Call {
|
||||||
_c.Call.Return(_a0, _a1)
|
_c.Call.Return(_a0, _a1)
|
||||||
return _c
|
return _c
|
||||||
}
|
}
|
||||||
|
|
||||||
func (_c *Store_GetAPIKey_Call) RunAndReturn(run func(context.Context, string) (database.ApiKey, error)) *Store_GetAPIKey_Call {
|
func (_c *Store_GetAPIKey_Call) RunAndReturn(run func(context.Context, string) (database.GetAPIKeyRow, error)) *Store_GetAPIKey_Call {
|
||||||
_c.Call.Return(run)
|
_c.Call.Return(run)
|
||||||
return _c
|
return _c
|
||||||
}
|
}
|
||||||
|
@ -1264,6 +1405,65 @@ func (_c *Store_GetCallAudioByID_Call) RunAndReturn(run func(context.Context, uu
|
||||||
return _c
|
return _c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetCallSubmitter provides a mock function with given fields: ctx, id
|
||||||
|
func (_m *Store) GetCallSubmitter(ctx context.Context, id uuid.UUID) (*int32, error) {
|
||||||
|
ret := _m.Called(ctx, id)
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for GetCallSubmitter")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 *int32
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) (*int32, error)); ok {
|
||||||
|
return rf(ctx, id)
|
||||||
|
}
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) *int32); ok {
|
||||||
|
r0 = rf(ctx, id)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*int32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID) error); ok {
|
||||||
|
r1 = rf(ctx, id)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store_GetCallSubmitter_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetCallSubmitter'
|
||||||
|
type Store_GetCallSubmitter_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCallSubmitter is a helper method to define mock.On call
|
||||||
|
// - ctx context.Context
|
||||||
|
// - id uuid.UUID
|
||||||
|
func (_e *Store_Expecter) GetCallSubmitter(ctx interface{}, id interface{}) *Store_GetCallSubmitter_Call {
|
||||||
|
return &Store_GetCallSubmitter_Call{Call: _e.mock.On("GetCallSubmitter", ctx, id)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *Store_GetCallSubmitter_Call) Run(run func(ctx context.Context, id uuid.UUID)) *Store_GetCallSubmitter_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
run(args[0].(context.Context), args[1].(uuid.UUID))
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *Store_GetCallSubmitter_Call) Return(_a0 *int32, _a1 error) *Store_GetCallSubmitter_Call {
|
||||||
|
_c.Call.Return(_a0, _a1)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *Store_GetCallSubmitter_Call) RunAndReturn(run func(context.Context, uuid.UUID) (*int32, error)) *Store_GetCallSubmitter_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
// GetDatabaseSize provides a mock function with given fields: ctx
|
// GetDatabaseSize provides a mock function with given fields: ctx
|
||||||
func (_m *Store) GetDatabaseSize(ctx context.Context) (string, error) {
|
func (_m *Store) GetDatabaseSize(ctx context.Context) (string, error) {
|
||||||
ret := _m.Called(ctx)
|
ret := _m.Called(ctx)
|
||||||
|
@ -1436,6 +1636,120 @@ func (_c *Store_GetIncidentCalls_Call) RunAndReturn(run func(context.Context, uu
|
||||||
return _c
|
return _c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetIncidentOwner provides a mock function with given fields: ctx, id
|
||||||
|
func (_m *Store) GetIncidentOwner(ctx context.Context, id uuid.UUID) (int, error) {
|
||||||
|
ret := _m.Called(ctx, id)
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for GetIncidentOwner")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 int
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) (int, error)); ok {
|
||||||
|
return rf(ctx, id)
|
||||||
|
}
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) int); ok {
|
||||||
|
r0 = rf(ctx, id)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Get(0).(int)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID) error); ok {
|
||||||
|
r1 = rf(ctx, id)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store_GetIncidentOwner_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetIncidentOwner'
|
||||||
|
type Store_GetIncidentOwner_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIncidentOwner is a helper method to define mock.On call
|
||||||
|
// - ctx context.Context
|
||||||
|
// - id uuid.UUID
|
||||||
|
func (_e *Store_Expecter) GetIncidentOwner(ctx interface{}, id interface{}) *Store_GetIncidentOwner_Call {
|
||||||
|
return &Store_GetIncidentOwner_Call{Call: _e.mock.On("GetIncidentOwner", ctx, id)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *Store_GetIncidentOwner_Call) Run(run func(ctx context.Context, id uuid.UUID)) *Store_GetIncidentOwner_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
run(args[0].(context.Context), args[1].(uuid.UUID))
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *Store_GetIncidentOwner_Call) Return(_a0 int, _a1 error) *Store_GetIncidentOwner_Call {
|
||||||
|
_c.Call.Return(_a0, _a1)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *Store_GetIncidentOwner_Call) RunAndReturn(run func(context.Context, uuid.UUID) (int, error)) *Store_GetIncidentOwner_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetShare provides a mock function with given fields: ctx, id
|
||||||
|
func (_m *Store) GetShare(ctx context.Context, id string) (database.Share, error) {
|
||||||
|
ret := _m.Called(ctx, id)
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for GetShare")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 database.Share
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, string) (database.Share, error)); ok {
|
||||||
|
return rf(ctx, id)
|
||||||
|
}
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, string) database.Share); ok {
|
||||||
|
r0 = rf(ctx, id)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Get(0).(database.Share)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||||
|
r1 = rf(ctx, id)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store_GetShare_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetShare'
|
||||||
|
type Store_GetShare_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetShare is a helper method to define mock.On call
|
||||||
|
// - ctx context.Context
|
||||||
|
// - id string
|
||||||
|
func (_e *Store_Expecter) GetShare(ctx interface{}, id interface{}) *Store_GetShare_Call {
|
||||||
|
return &Store_GetShare_Call{Call: _e.mock.On("GetShare", ctx, id)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *Store_GetShare_Call) Run(run func(ctx context.Context, id string)) *Store_GetShare_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
run(args[0].(context.Context), args[1].(string))
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *Store_GetShare_Call) Return(_a0 database.Share, _a1 error) *Store_GetShare_Call {
|
||||||
|
_c.Call.Return(_a0, _a1)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *Store_GetShare_Call) RunAndReturn(run func(context.Context, string) (database.Share, error)) *Store_GetShare_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
// GetSystemName provides a mock function with given fields: ctx, systemID
|
// GetSystemName provides a mock function with given fields: ctx, systemID
|
||||||
func (_m *Store) GetSystemName(ctx context.Context, systemID int) (string, error) {
|
func (_m *Store) GetSystemName(ctx context.Context, systemID int) (string, error) {
|
||||||
ret := _m.Called(ctx, systemID)
|
ret := _m.Called(ctx, systemID)
|
||||||
|
@ -2433,63 +2747,6 @@ func (_c *Store_GetUserByID_Call) RunAndReturn(run func(context.Context, int) (d
|
||||||
return _c
|
return _c
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserByUID provides a mock function with given fields: ctx, id
|
|
||||||
func (_m *Store) GetUserByUID(ctx context.Context, id int) (database.User, error) {
|
|
||||||
ret := _m.Called(ctx, id)
|
|
||||||
|
|
||||||
if len(ret) == 0 {
|
|
||||||
panic("no return value specified for GetUserByUID")
|
|
||||||
}
|
|
||||||
|
|
||||||
var r0 database.User
|
|
||||||
var r1 error
|
|
||||||
if rf, ok := ret.Get(0).(func(context.Context, int) (database.User, error)); ok {
|
|
||||||
return rf(ctx, id)
|
|
||||||
}
|
|
||||||
if rf, ok := ret.Get(0).(func(context.Context, int) database.User); ok {
|
|
||||||
r0 = rf(ctx, id)
|
|
||||||
} else {
|
|
||||||
r0 = ret.Get(0).(database.User)
|
|
||||||
}
|
|
||||||
|
|
||||||
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
|
|
||||||
r1 = rf(ctx, id)
|
|
||||||
} else {
|
|
||||||
r1 = ret.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0, r1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store_GetUserByUID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetUserByUID'
|
|
||||||
type Store_GetUserByUID_Call struct {
|
|
||||||
*mock.Call
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUserByUID is a helper method to define mock.On call
|
|
||||||
// - ctx context.Context
|
|
||||||
// - id int
|
|
||||||
func (_e *Store_Expecter) GetUserByUID(ctx interface{}, id interface{}) *Store_GetUserByUID_Call {
|
|
||||||
return &Store_GetUserByUID_Call{Call: _e.mock.On("GetUserByUID", ctx, id)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *Store_GetUserByUID_Call) Run(run func(ctx context.Context, id int)) *Store_GetUserByUID_Call {
|
|
||||||
_c.Call.Run(func(args mock.Arguments) {
|
|
||||||
run(args[0].(context.Context), args[1].(int))
|
|
||||||
})
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *Store_GetUserByUID_Call) Return(_a0 database.User, _a1 error) *Store_GetUserByUID_Call {
|
|
||||||
_c.Call.Return(_a0, _a1)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_c *Store_GetUserByUID_Call) RunAndReturn(run func(context.Context, int) (database.User, error)) *Store_GetUserByUID_Call {
|
|
||||||
_c.Call.Return(run)
|
|
||||||
return _c
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUserByUsername provides a mock function with given fields: ctx, username
|
// GetUserByUsername provides a mock function with given fields: ctx, username
|
||||||
func (_m *Store) GetUserByUsername(ctx context.Context, username string) (database.User, error) {
|
func (_m *Store) GetUserByUsername(ctx context.Context, username string) (database.User, error) {
|
||||||
ret := _m.Called(ctx, username)
|
ret := _m.Called(ctx, username)
|
||||||
|
@ -2887,6 +3144,52 @@ func (_c *Store_ListIncidentsP_Call) RunAndReturn(run func(context.Context, data
|
||||||
return _c
|
return _c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PruneShares provides a mock function with given fields: ctx
|
||||||
|
func (_m *Store) PruneShares(ctx context.Context) error {
|
||||||
|
ret := _m.Called(ctx)
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for PruneShares")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context) error); ok {
|
||||||
|
r0 = rf(ctx)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store_PruneShares_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PruneShares'
|
||||||
|
type Store_PruneShares_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// PruneShares is a helper method to define mock.On call
|
||||||
|
// - ctx context.Context
|
||||||
|
func (_e *Store_Expecter) PruneShares(ctx interface{}) *Store_PruneShares_Call {
|
||||||
|
return &Store_PruneShares_Call{Call: _e.mock.On("PruneShares", ctx)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *Store_PruneShares_Call) Run(run func(ctx context.Context)) *Store_PruneShares_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
run(args[0].(context.Context))
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *Store_PruneShares_Call) Return(_a0 error) *Store_PruneShares_Call {
|
||||||
|
_c.Call.Return(_a0)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *Store_PruneShares_Call) RunAndReturn(run func(context.Context) error) *Store_PruneShares_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
// RemoveFromIncident provides a mock function with given fields: ctx, iD, callIds
|
// RemoveFromIncident provides a mock function with given fields: ctx, iD, callIds
|
||||||
func (_m *Store) RemoveFromIncident(ctx context.Context, iD uuid.UUID, callIds []uuid.UUID) error {
|
func (_m *Store) RemoveFromIncident(ctx context.Context, iD uuid.UUID, callIds []uuid.UUID) error {
|
||||||
ret := _m.Called(ctx, iD, callIds)
|
ret := _m.Called(ctx, iD, callIds)
|
||||||
|
@ -3505,6 +3808,65 @@ func (_c *Store_UpdateTalkgroup_Call) RunAndReturn(run func(context.Context, dat
|
||||||
return _c
|
return _c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateUser provides a mock function with given fields: ctx, username, email, isAdmin
|
||||||
|
func (_m *Store) UpdateUser(ctx context.Context, username string, email *string, isAdmin *bool) (database.User, error) {
|
||||||
|
ret := _m.Called(ctx, username, email, isAdmin)
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for UpdateUser")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 database.User
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, string, *string, *bool) (database.User, error)); ok {
|
||||||
|
return rf(ctx, username, email, isAdmin)
|
||||||
|
}
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, string, *string, *bool) database.User); ok {
|
||||||
|
r0 = rf(ctx, username, email, isAdmin)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Get(0).(database.User)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, string, *string, *bool) error); ok {
|
||||||
|
r1 = rf(ctx, username, email, isAdmin)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store_UpdateUser_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUser'
|
||||||
|
type Store_UpdateUser_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUser is a helper method to define mock.On call
|
||||||
|
// - ctx context.Context
|
||||||
|
// - username string
|
||||||
|
// - email *string
|
||||||
|
// - isAdmin *bool
|
||||||
|
func (_e *Store_Expecter) UpdateUser(ctx interface{}, username interface{}, email interface{}, isAdmin interface{}) *Store_UpdateUser_Call {
|
||||||
|
return &Store_UpdateUser_Call{Call: _e.mock.On("UpdateUser", ctx, username, email, isAdmin)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *Store_UpdateUser_Call) Run(run func(ctx context.Context, username string, email *string, isAdmin *bool)) *Store_UpdateUser_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
run(args[0].(context.Context), args[1].(string), args[2].(*string), args[3].(*bool))
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *Store_UpdateUser_Call) Return(_a0 database.User, _a1 error) *Store_UpdateUser_Call {
|
||||||
|
_c.Call.Return(_a0, _a1)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *Store_UpdateUser_Call) RunAndReturn(run func(context.Context, string, *string, *bool) (database.User, error)) *Store_UpdateUser_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
// UpsertTalkgroup provides a mock function with given fields: ctx, arg
|
// UpsertTalkgroup provides a mock function with given fields: ctx, arg
|
||||||
func (_m *Store) UpsertTalkgroup(ctx context.Context, arg []database.UpsertTalkgroupParams) *database.UpsertTalkgroupBatchResults {
|
func (_m *Store) UpsertTalkgroup(ctx context.Context, arg []database.UpsertTalkgroupParams) *database.UpsertTalkgroupBatchResults {
|
||||||
ret := _m.Called(ctx, arg)
|
ret := _m.Called(ctx, arg)
|
||||||
|
|
|
@ -58,6 +58,7 @@ type Call struct {
|
||||||
type Incident struct {
|
type Incident struct {
|
||||||
ID uuid.UUID `json:"id,omitempty"`
|
ID uuid.UUID `json:"id,omitempty"`
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
|
Owner int `json:"owner,omitempty"`
|
||||||
Description *string `json:"description,omitempty"`
|
Description *string `json:"description,omitempty"`
|
||||||
StartTime pgtype.Timestamptz `json:"start_time,omitempty"`
|
StartTime pgtype.Timestamptz `json:"start_time,omitempty"`
|
||||||
EndTime pgtype.Timestamptz `json:"end_time,omitempty"`
|
EndTime pgtype.Timestamptz `json:"end_time,omitempty"`
|
||||||
|
@ -80,6 +81,14 @@ type Setting struct {
|
||||||
Value []byte `json:"value,omitempty"`
|
Value []byte `json:"value,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Share struct {
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
EntityType string `json:"entity_type,omitempty"`
|
||||||
|
EntityID uuid.UUID `json:"entity_id,omitempty"`
|
||||||
|
Owner int `json:"owner,omitempty"`
|
||||||
|
Expiration pgtype.Timestamptz `json:"expiration,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type SweptCall struct {
|
type SweptCall struct {
|
||||||
ID uuid.UUID `json:"id,omitempty"`
|
ID uuid.UUID `json:"id,omitempty"`
|
||||||
Submitter *int32 `json:"submitter,omitempty"`
|
Submitter *int32 `json:"submitter,omitempty"`
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"dynatron.me/x/stillbox/internal/isoweek"
|
"dynatron.me/x/stillbox/internal/isoweek"
|
||||||
"dynatron.me/x/stillbox/pkg/config"
|
"dynatron.me/x/stillbox/pkg/config"
|
||||||
"dynatron.me/x/stillbox/pkg/database"
|
"dynatron.me/x/stillbox/pkg/database"
|
||||||
|
"dynatron.me/x/stillbox/pkg/rbac"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
@ -134,6 +135,7 @@ func New(db database.Store, cfg config.Partition) (*partman, error) {
|
||||||
var _ PartitionManager = (*partman)(nil)
|
var _ PartitionManager = (*partman)(nil)
|
||||||
|
|
||||||
func (pm *partman) Go(ctx context.Context) {
|
func (pm *partman) Go(ctx context.Context) {
|
||||||
|
ctx = rbac.CtxWithSubject(ctx, &rbac.SystemServiceSubject{Name: "partman"})
|
||||||
tick := time.NewTicker(CheckInterval)
|
tick := time.NewTicker(CheckInterval)
|
||||||
|
|
||||||
select {
|
select {
|
||||||
|
|
|
@ -19,20 +19,26 @@ type Querier interface {
|
||||||
CleanupSweptCalls(ctx context.Context, rangeStart pgtype.Timestamptz, rangeEnd pgtype.Timestamptz) (int64, error)
|
CleanupSweptCalls(ctx context.Context, rangeStart pgtype.Timestamptz, rangeEnd pgtype.Timestamptz) (int64, error)
|
||||||
CreateAPIKey(ctx context.Context, owner int, expires pgtype.Timestamp, disabled *bool) (ApiKey, error)
|
CreateAPIKey(ctx context.Context, owner int, expires pgtype.Timestamp, disabled *bool) (ApiKey, error)
|
||||||
CreateIncident(ctx context.Context, arg CreateIncidentParams) (Incident, error)
|
CreateIncident(ctx context.Context, arg CreateIncidentParams) (Incident, error)
|
||||||
|
CreateShare(ctx context.Context, arg CreateShareParams) error
|
||||||
CreateSystem(ctx context.Context, iD int, name string) error
|
CreateSystem(ctx context.Context, iD int, name string) error
|
||||||
CreateUser(ctx context.Context, arg CreateUserParams) (User, error)
|
CreateUser(ctx context.Context, arg CreateUserParams) (User, error)
|
||||||
DeleteAPIKey(ctx context.Context, apiKey string) error
|
DeleteAPIKey(ctx context.Context, apiKey string) error
|
||||||
|
DeleteCall(ctx context.Context, id uuid.UUID) error
|
||||||
DeleteIncident(ctx context.Context, id uuid.UUID) error
|
DeleteIncident(ctx context.Context, id uuid.UUID) error
|
||||||
|
DeleteShare(ctx context.Context, id string) error
|
||||||
DeleteSystem(ctx context.Context, id int) error
|
DeleteSystem(ctx context.Context, id int) error
|
||||||
DeleteTalkgroup(ctx context.Context, systemID int32, tGID int32) error
|
DeleteTalkgroup(ctx context.Context, systemID int32, tGID int32) error
|
||||||
DeleteUser(ctx context.Context, username string) error
|
DeleteUser(ctx context.Context, username string) error
|
||||||
GetAPIKey(ctx context.Context, apiKey string) (ApiKey, error)
|
GetAPIKey(ctx context.Context, apiKey string) (GetAPIKeyRow, error)
|
||||||
GetAllTalkgroupTags(ctx context.Context) ([]string, error)
|
GetAllTalkgroupTags(ctx context.Context) ([]string, error)
|
||||||
GetAppPrefs(ctx context.Context, appName string, uid int) ([]byte, error)
|
GetAppPrefs(ctx context.Context, appName string, uid int) ([]byte, error)
|
||||||
GetCallAudioByID(ctx context.Context, id uuid.UUID) (GetCallAudioByIDRow, error)
|
GetCallAudioByID(ctx context.Context, id uuid.UUID) (GetCallAudioByIDRow, error)
|
||||||
|
GetCallSubmitter(ctx context.Context, id uuid.UUID) (*int32, error)
|
||||||
GetDatabaseSize(ctx context.Context) (string, error)
|
GetDatabaseSize(ctx context.Context) (string, error)
|
||||||
GetIncident(ctx context.Context, id uuid.UUID) (Incident, error)
|
GetIncident(ctx context.Context, id uuid.UUID) (Incident, error)
|
||||||
GetIncidentCalls(ctx context.Context, id uuid.UUID) ([]GetIncidentCallsRow, error)
|
GetIncidentCalls(ctx context.Context, id uuid.UUID) ([]GetIncidentCallsRow, error)
|
||||||
|
GetIncidentOwner(ctx context.Context, id uuid.UUID) (int, error)
|
||||||
|
GetShare(ctx context.Context, id string) (Share, error)
|
||||||
GetSystemName(ctx context.Context, systemID int) (string, error)
|
GetSystemName(ctx context.Context, systemID int) (string, error)
|
||||||
GetTalkgroup(ctx context.Context, systemID int32, tGID int32) (GetTalkgroupRow, error)
|
GetTalkgroup(ctx context.Context, systemID int32, tGID int32) (GetTalkgroupRow, error)
|
||||||
GetTalkgroupIDsByTags(ctx context.Context, anyTags []string, allTags []string, notTags []string) ([]GetTalkgroupIDsByTagsRow, error)
|
GetTalkgroupIDsByTags(ctx context.Context, anyTags []string, allTags []string, notTags []string) ([]GetTalkgroupIDsByTagsRow, error)
|
||||||
|
@ -47,13 +53,13 @@ type Querier interface {
|
||||||
GetTalkgroupsWithLearnedCount(ctx context.Context, filter *string) (int64, error)
|
GetTalkgroupsWithLearnedCount(ctx context.Context, filter *string) (int64, error)
|
||||||
GetTalkgroupsWithLearnedP(ctx context.Context, arg GetTalkgroupsWithLearnedPParams) ([]GetTalkgroupsWithLearnedPRow, error)
|
GetTalkgroupsWithLearnedP(ctx context.Context, arg GetTalkgroupsWithLearnedPParams) ([]GetTalkgroupsWithLearnedPRow, error)
|
||||||
GetUserByID(ctx context.Context, id int) (User, error)
|
GetUserByID(ctx context.Context, id int) (User, error)
|
||||||
GetUserByUID(ctx context.Context, id int) (User, error)
|
|
||||||
GetUserByUsername(ctx context.Context, username string) (User, error)
|
GetUserByUsername(ctx context.Context, username string) (User, error)
|
||||||
GetUsers(ctx context.Context) ([]User, error)
|
GetUsers(ctx context.Context) ([]User, error)
|
||||||
ListCallsCount(ctx context.Context, arg ListCallsCountParams) (int64, error)
|
ListCallsCount(ctx context.Context, arg ListCallsCountParams) (int64, error)
|
||||||
ListCallsP(ctx context.Context, arg ListCallsPParams) ([]ListCallsPRow, error)
|
ListCallsP(ctx context.Context, arg ListCallsPParams) ([]ListCallsPRow, error)
|
||||||
ListIncidentsCount(ctx context.Context, start pgtype.Timestamptz, end pgtype.Timestamptz, filter *string) (int64, error)
|
ListIncidentsCount(ctx context.Context, start pgtype.Timestamptz, end pgtype.Timestamptz, filter *string) (int64, error)
|
||||||
ListIncidentsP(ctx context.Context, arg ListIncidentsPParams) ([]ListIncidentsPRow, error)
|
ListIncidentsP(ctx context.Context, arg ListIncidentsPParams) ([]ListIncidentsPRow, error)
|
||||||
|
PruneShares(ctx context.Context) error
|
||||||
RemoveFromIncident(ctx context.Context, iD uuid.UUID, callIds []uuid.UUID) error
|
RemoveFromIncident(ctx context.Context, iD uuid.UUID, callIds []uuid.UUID) error
|
||||||
RestoreTalkgroupVersion(ctx context.Context, versionIds int) (Talkgroup, error)
|
RestoreTalkgroupVersion(ctx context.Context, versionIds int) (Talkgroup, error)
|
||||||
SetAppPrefs(ctx context.Context, appName string, prefs []byte, uid int) error
|
SetAppPrefs(ctx context.Context, appName string, prefs []byte, uid int) error
|
||||||
|
@ -67,6 +73,7 @@ type Querier interface {
|
||||||
UpdateIncident(ctx context.Context, arg UpdateIncidentParams) (Incident, error)
|
UpdateIncident(ctx context.Context, arg UpdateIncidentParams) (Incident, error)
|
||||||
UpdatePassword(ctx context.Context, username string, password string) error
|
UpdatePassword(ctx context.Context, username string, password string) error
|
||||||
UpdateTalkgroup(ctx context.Context, arg UpdateTalkgroupParams) (Talkgroup, error)
|
UpdateTalkgroup(ctx context.Context, arg UpdateTalkgroupParams) (Talkgroup, error)
|
||||||
|
UpdateUser(ctx context.Context, username string, email *string, isAdmin *bool) (User, error)
|
||||||
UpsertTalkgroup(ctx context.Context, arg []UpsertTalkgroupParams) *UpsertTalkgroupBatchResults
|
UpsertTalkgroup(ctx context.Context, arg []UpsertTalkgroupParams) *UpsertTalkgroupBatchResults
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
84
pkg/database/share.sql.go
Normal file
84
pkg/database/share.sql.go
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.27.0
|
||||||
|
// source: share.sql
|
||||||
|
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
const createShare = `-- name: CreateShare :exec
|
||||||
|
INSERT INTO shares (
|
||||||
|
id,
|
||||||
|
entity_type,
|
||||||
|
entity_id,
|
||||||
|
owner,
|
||||||
|
expiration
|
||||||
|
) VALUES ($1, $2, $3, $4, $5)
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateShareParams struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
EntityType string `json:"entity_type"`
|
||||||
|
EntityID uuid.UUID `json:"entity_id"`
|
||||||
|
Owner int `json:"owner"`
|
||||||
|
Expiration pgtype.Timestamptz `json:"expiration"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateShare(ctx context.Context, arg CreateShareParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, createShare,
|
||||||
|
arg.ID,
|
||||||
|
arg.EntityType,
|
||||||
|
arg.EntityID,
|
||||||
|
arg.Owner,
|
||||||
|
arg.Expiration,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteShare = `-- name: DeleteShare :exec
|
||||||
|
DELETE FROM shares WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteShare(ctx context.Context, id string) error {
|
||||||
|
_, err := q.db.Exec(ctx, deleteShare, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getShare = `-- name: GetShare :one
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
entity_type,
|
||||||
|
entity_id,
|
||||||
|
owner,
|
||||||
|
expiration
|
||||||
|
FROM shares
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetShare(ctx context.Context, id string) (Share, error) {
|
||||||
|
row := q.db.QueryRow(ctx, getShare, id)
|
||||||
|
var i Share
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.EntityType,
|
||||||
|
&i.EntityID,
|
||||||
|
&i.Owner,
|
||||||
|
&i.Expiration,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const pruneShares = `-- name: PruneShares :exec
|
||||||
|
DELETE FROM shares WHERE expiration < NOW()
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) PruneShares(ctx context.Context) error {
|
||||||
|
_, err := q.db.Exec(ctx, pruneShares)
|
||||||
|
return err
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
|
@ -91,12 +92,32 @@ func (q *Queries) DeleteUser(ctx context.Context, username string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAPIKey = `-- name: GetAPIKey :one
|
const getAPIKey = `-- name: GetAPIKey :one
|
||||||
SELECT id, owner, created_at, expires, disabled, api_key FROM api_keys WHERE api_key = $1
|
SELECT
|
||||||
|
a.id,
|
||||||
|
a.owner,
|
||||||
|
a.created_at,
|
||||||
|
a.expires,
|
||||||
|
a.disabled,
|
||||||
|
a.api_key,
|
||||||
|
u.username
|
||||||
|
FROM api_keys a
|
||||||
|
JOIN users u ON (a.owner = u.id)
|
||||||
|
WHERE api_key = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetAPIKey(ctx context.Context, apiKey string) (ApiKey, error) {
|
type GetAPIKeyRow struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Owner int `json:"owner"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
Expires pgtype.Timestamp `json:"expires"`
|
||||||
|
Disabled *bool `json:"disabled"`
|
||||||
|
ApiKey string `json:"api_key"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetAPIKey(ctx context.Context, apiKey string) (GetAPIKeyRow, error) {
|
||||||
row := q.db.QueryRow(ctx, getAPIKey, apiKey)
|
row := q.db.QueryRow(ctx, getAPIKey, apiKey)
|
||||||
var i ApiKey
|
var i GetAPIKeyRow
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.Owner,
|
&i.Owner,
|
||||||
|
@ -104,6 +125,7 @@ func (q *Queries) GetAPIKey(ctx context.Context, apiKey string) (ApiKey, error)
|
||||||
&i.Expires,
|
&i.Expires,
|
||||||
&i.Disabled,
|
&i.Disabled,
|
||||||
&i.ApiKey,
|
&i.ApiKey,
|
||||||
|
&i.Username,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
@ -121,7 +143,7 @@ func (q *Queries) GetAppPrefs(ctx context.Context, appName string, uid int) ([]b
|
||||||
|
|
||||||
const getUserByID = `-- name: GetUserByID :one
|
const getUserByID = `-- name: GetUserByID :one
|
||||||
SELECT id, username, password, email, is_admin, prefs FROM users
|
SELECT id, username, password, email, is_admin, prefs FROM users
|
||||||
WHERE id = $1 LIMIT 1
|
WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetUserByID(ctx context.Context, id int) (User, error) {
|
func (q *Queries) GetUserByID(ctx context.Context, id int) (User, error) {
|
||||||
|
@ -138,28 +160,9 @@ func (q *Queries) GetUserByID(ctx context.Context, id int) (User, error) {
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const getUserByUID = `-- name: GetUserByUID :one
|
|
||||||
SELECT id, username, password, email, is_admin, prefs FROM users
|
|
||||||
WHERE id = $1 LIMIT 1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetUserByUID(ctx context.Context, id int) (User, error) {
|
|
||||||
row := q.db.QueryRow(ctx, getUserByUID, id)
|
|
||||||
var i User
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.Username,
|
|
||||||
&i.Password,
|
|
||||||
&i.Email,
|
|
||||||
&i.IsAdmin,
|
|
||||||
&i.Prefs,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const getUserByUsername = `-- name: GetUserByUsername :one
|
const getUserByUsername = `-- name: GetUserByUsername :one
|
||||||
SELECT id, username, password, email, is_admin, prefs FROM users
|
SELECT id, username, password, email, is_admin, prefs FROM users
|
||||||
WHERE username = $1 LIMIT 1
|
WHERE username = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetUserByUsername(ctx context.Context, username string) (User, error) {
|
func (q *Queries) GetUserByUsername(ctx context.Context, username string) (User, error) {
|
||||||
|
@ -224,3 +227,26 @@ func (q *Queries) UpdatePassword(ctx context.Context, username string, password
|
||||||
_, err := q.db.Exec(ctx, updatePassword, username, password)
|
_, err := q.db.Exec(ctx, updatePassword, username, password)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateUser = `-- name: UpdateUser :one
|
||||||
|
UPDATE users SET
|
||||||
|
email = COALESCE($2, email),
|
||||||
|
is_admin = COALESCE($3, is_admin)
|
||||||
|
WHERE
|
||||||
|
username = $1
|
||||||
|
RETURNING id, username, password, email, is_admin, prefs
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) UpdateUser(ctx context.Context, username string, email *string, isAdmin *bool) (User, error) {
|
||||||
|
row := q.db.QueryRow(ctx, updateUser, username, email, isAdmin)
|
||||||
|
var i User
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Username,
|
||||||
|
&i.Password,
|
||||||
|
&i.Email,
|
||||||
|
&i.IsAdmin,
|
||||||
|
&i.Prefs,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
|
@ -5,11 +5,14 @@ import (
|
||||||
|
|
||||||
"dynatron.me/x/stillbox/internal/jsontypes"
|
"dynatron.me/x/stillbox/internal/jsontypes"
|
||||||
"dynatron.me/x/stillbox/pkg/calls"
|
"dynatron.me/x/stillbox/pkg/calls"
|
||||||
|
"dynatron.me/x/stillbox/pkg/rbac"
|
||||||
|
"dynatron.me/x/stillbox/pkg/users"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Incident struct {
|
type Incident struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
|
Owner users.UserID `json:"owner"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description *string `json:"description"`
|
Description *string `json:"description"`
|
||||||
StartTime *jsontypes.Time `json:"startTime"`
|
StartTime *jsontypes.Time `json:"startTime"`
|
||||||
|
@ -19,6 +22,10 @@ type Incident struct {
|
||||||
Calls []IncidentCall `json:"calls"`
|
Calls []IncidentCall `json:"calls"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (inc *Incident) GetResourceName() string {
|
||||||
|
return rbac.ResourceIncident
|
||||||
|
}
|
||||||
|
|
||||||
type IncidentCall struct {
|
type IncidentCall struct {
|
||||||
calls.Call
|
calls.Call
|
||||||
Notes json.RawMessage `json:"notes"`
|
Notes json.RawMessage `json:"notes"`
|
||||||
|
|
|
@ -6,10 +6,11 @@ import (
|
||||||
|
|
||||||
"dynatron.me/x/stillbox/internal/common"
|
"dynatron.me/x/stillbox/internal/common"
|
||||||
"dynatron.me/x/stillbox/internal/jsontypes"
|
"dynatron.me/x/stillbox/internal/jsontypes"
|
||||||
"dynatron.me/x/stillbox/pkg/auth"
|
|
||||||
"dynatron.me/x/stillbox/pkg/calls"
|
"dynatron.me/x/stillbox/pkg/calls"
|
||||||
"dynatron.me/x/stillbox/pkg/database"
|
"dynatron.me/x/stillbox/pkg/database"
|
||||||
"dynatron.me/x/stillbox/pkg/incidents"
|
"dynatron.me/x/stillbox/pkg/incidents"
|
||||||
|
"dynatron.me/x/stillbox/pkg/rbac"
|
||||||
|
"dynatron.me/x/stillbox/pkg/users"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
)
|
)
|
||||||
|
@ -72,6 +73,11 @@ func NewStore() Store {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *store) CreateIncident(ctx context.Context, inc incidents.Incident) (*incidents.Incident, error) {
|
func (s *store) CreateIncident(ctx context.Context, inc incidents.Incident) (*incidents.Incident, error) {
|
||||||
|
user, err := users.UserCheck(ctx, new(incidents.Incident), "create")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
db := database.FromCtx(ctx)
|
db := database.FromCtx(ctx)
|
||||||
var dbInc database.Incident
|
var dbInc database.Incident
|
||||||
|
|
||||||
|
@ -81,6 +87,7 @@ func (s *store) CreateIncident(ctx context.Context, inc incidents.Incident) (*in
|
||||||
var err error
|
var err error
|
||||||
dbInc, err = db.CreateIncident(ctx, database.CreateIncidentParams{
|
dbInc, err = db.CreateIncident(ctx, database.CreateIncidentParams{
|
||||||
ID: id,
|
ID: id,
|
||||||
|
Owner: user.ID.Int(),
|
||||||
Name: inc.Name,
|
Name: inc.Name,
|
||||||
Description: inc.Description,
|
Description: inc.Description,
|
||||||
StartTime: inc.StartTime.PGTypeTSTZ(),
|
StartTime: inc.StartTime.PGTypeTSTZ(),
|
||||||
|
@ -125,6 +132,16 @@ func (s *store) CreateIncident(ctx context.Context, inc incidents.Incident) (*in
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *store) AddRemoveIncidentCalls(ctx context.Context, incidentID uuid.UUID, addCallIDs []uuid.UUID, notes []byte, removeCallIDs []uuid.UUID) error {
|
func (s *store) AddRemoveIncidentCalls(ctx context.Context, incidentID uuid.UUID, addCallIDs []uuid.UUID, notes []byte, removeCallIDs []uuid.UUID) error {
|
||||||
|
inc, err := s.getIncidentOwner(ctx, incidentID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = rbac.Check(ctx, &inc, rbac.WithActions(rbac.ActionUpdate))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return database.FromCtx(ctx).InTx(ctx, func(db database.Store) error {
|
return database.FromCtx(ctx).InTx(ctx, func(db database.Store) error {
|
||||||
if len(addCallIDs) > 0 {
|
if len(addCallIDs) > 0 {
|
||||||
var noteAr [][]byte
|
var noteAr [][]byte
|
||||||
|
@ -153,6 +170,10 @@ func (s *store) AddRemoveIncidentCalls(ctx context.Context, incidentID uuid.UUID
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *store) Incidents(ctx context.Context, p IncidentsParams) (incs []Incident, totalCount int, err error) {
|
func (s *store) Incidents(ctx context.Context, p IncidentsParams) (incs []Incident, totalCount int, err error) {
|
||||||
|
_, err = rbac.Check(ctx, new(incidents.Incident), rbac.WithActions(rbac.ActionRead))
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
db := database.FromCtx(ctx)
|
db := database.FromCtx(ctx)
|
||||||
|
|
||||||
offset, perPage := p.Pagination.OffsetPerPage(100)
|
offset, perPage := p.Pagination.OffsetPerPage(100)
|
||||||
|
@ -196,6 +217,7 @@ func (s *store) Incidents(ctx context.Context, p IncidentsParams) (incs []Incide
|
||||||
func fromDBIncident(id uuid.UUID, d database.Incident) incidents.Incident {
|
func fromDBIncident(id uuid.UUID, d database.Incident) incidents.Incident {
|
||||||
return incidents.Incident{
|
return incidents.Incident{
|
||||||
ID: id,
|
ID: id,
|
||||||
|
Owner: users.UserID(d.Owner),
|
||||||
Name: d.Name,
|
Name: d.Name,
|
||||||
Description: d.Description,
|
Description: d.Description,
|
||||||
StartTime: jsontypes.TimePtrFromTSTZ(d.StartTime),
|
StartTime: jsontypes.TimePtrFromTSTZ(d.StartTime),
|
||||||
|
@ -214,6 +236,7 @@ func fromDBListInPRow(id uuid.UUID, d database.ListIncidentsPRow) Incident {
|
||||||
return Incident{
|
return Incident{
|
||||||
Incident: incidents.Incident{
|
Incident: incidents.Incident{
|
||||||
ID: id,
|
ID: id,
|
||||||
|
Owner: users.UserID(d.Owner),
|
||||||
Name: d.Name,
|
Name: d.Name,
|
||||||
Description: d.Description,
|
Description: d.Description,
|
||||||
StartTime: jsontypes.TimePtrFromTSTZ(d.StartTime),
|
StartTime: jsontypes.TimePtrFromTSTZ(d.StartTime),
|
||||||
|
@ -228,7 +251,7 @@ func fromDBCalls(d []database.GetIncidentCallsRow) []incidents.IncidentCall {
|
||||||
r := make([]incidents.IncidentCall, 0, len(d))
|
r := make([]incidents.IncidentCall, 0, len(d))
|
||||||
for _, v := range d {
|
for _, v := range d {
|
||||||
dur := calls.CallDuration(time.Duration(common.ZeroIfNil(v.Duration)) * time.Millisecond)
|
dur := calls.CallDuration(time.Duration(common.ZeroIfNil(v.Duration)) * time.Millisecond)
|
||||||
sub := common.PtrTo(auth.UserID(common.ZeroIfNil(v.Submitter)))
|
sub := common.PtrTo(users.UserID(common.ZeroIfNil(v.Submitter)))
|
||||||
r = append(r, incidents.IncidentCall{
|
r = append(r, incidents.IncidentCall{
|
||||||
Call: calls.Call{
|
Call: calls.Call{
|
||||||
ID: v.CallID,
|
ID: v.CallID,
|
||||||
|
@ -252,6 +275,11 @@ func fromDBCalls(d []database.GetIncidentCallsRow) []incidents.IncidentCall {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *store) Incident(ctx context.Context, id uuid.UUID) (*incidents.Incident, error) {
|
func (s *store) Incident(ctx context.Context, id uuid.UUID) (*incidents.Incident, error) {
|
||||||
|
_, err := rbac.Check(ctx, new(incidents.Incident), rbac.WithActions(rbac.ActionRead))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
var r incidents.Incident
|
var r incidents.Incident
|
||||||
txErr := database.FromCtx(ctx).InTx(ctx, func(db database.Store) error {
|
txErr := database.FromCtx(ctx).InTx(ctx, func(db database.Store) error {
|
||||||
inc, err := db.GetIncident(ctx, id)
|
inc, err := db.GetIncident(ctx, id)
|
||||||
|
@ -298,6 +326,16 @@ func (uip UpdateIncidentParams) toDBUIP(id uuid.UUID) database.UpdateIncidentPar
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *store) UpdateIncident(ctx context.Context, id uuid.UUID, p UpdateIncidentParams) (*incidents.Incident, error) {
|
func (s *store) UpdateIncident(ctx context.Context, id uuid.UUID, p UpdateIncidentParams) (*incidents.Incident, error) {
|
||||||
|
ckinc, err := s.getIncidentOwner(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = rbac.Check(ctx, &ckinc, rbac.WithActions(rbac.ActionUpdate))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
db := database.FromCtx(ctx)
|
db := database.FromCtx(ctx)
|
||||||
|
|
||||||
dbInc, err := db.UpdateIncident(ctx, p.toDBUIP(id))
|
dbInc, err := db.UpdateIncident(ctx, p.toDBUIP(id))
|
||||||
|
@ -311,9 +349,24 @@ func (s *store) UpdateIncident(ctx context.Context, id uuid.UUID, p UpdateIncide
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *store) DeleteIncident(ctx context.Context, id uuid.UUID) error {
|
func (s *store) DeleteIncident(ctx context.Context, id uuid.UUID) error {
|
||||||
|
inc, err := s.getIncidentOwner(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = rbac.Check(ctx, &inc, rbac.WithActions(rbac.ActionDelete))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return database.FromCtx(ctx).DeleteIncident(ctx, id)
|
return database.FromCtx(ctx).DeleteIncident(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *store) UpdateNotes(ctx context.Context, incidentID uuid.UUID, callID uuid.UUID, notes []byte) error {
|
func (s *store) UpdateNotes(ctx context.Context, incidentID uuid.UUID, callID uuid.UUID, notes []byte) error {
|
||||||
return database.FromCtx(ctx).UpdateCallIncidentNotes(ctx, notes, incidentID, callID)
|
return database.FromCtx(ctx).UpdateCallIncidentNotes(ctx, notes, incidentID, callID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *store) getIncidentOwner(ctx context.Context, id uuid.UUID) (incidents.Incident, error) {
|
||||||
|
owner, err := database.FromCtx(ctx).GetIncidentOwner(ctx, id)
|
||||||
|
return incidents.Incident{ID: id, Owner: users.UserID(owner)}, err
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
|
|
||||||
"dynatron.me/x/stillbox/pkg/calls"
|
"dynatron.me/x/stillbox/pkg/calls"
|
||||||
"dynatron.me/x/stillbox/pkg/pb"
|
"dynatron.me/x/stillbox/pkg/pb"
|
||||||
|
"dynatron.me/x/stillbox/pkg/rbac"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
@ -38,6 +39,7 @@ func New() *Nexus {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Nexus) Go(ctx context.Context) {
|
func (n *Nexus) Go(ctx context.Context) {
|
||||||
|
ctx = rbac.CtxWithSubject(ctx, &rbac.SystemServiceSubject{Name: "nexus"})
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case call, ok := <-n.callCh:
|
case call, ok := <-n.callCh:
|
||||||
|
|
113
pkg/rbac/mocks/RBAC.go
Normal file
113
pkg/rbac/mocks/RBAC.go
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
// Code generated by mockery v2.47.0. DO NOT EDIT.
|
||||||
|
|
||||||
|
package mocks
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
|
||||||
|
rbac "dynatron.me/x/stillbox/pkg/rbac"
|
||||||
|
mock "github.com/stretchr/testify/mock"
|
||||||
|
|
||||||
|
restrict "github.com/el-mike/restrict/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RBAC is an autogenerated mock type for the RBAC type
|
||||||
|
type RBAC struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
type RBAC_Expecter struct {
|
||||||
|
mock *mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_m *RBAC) EXPECT() *RBAC_Expecter {
|
||||||
|
return &RBAC_Expecter{mock: &_m.Mock}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check provides a mock function with given fields: ctx, res, opts
|
||||||
|
func (_m *RBAC) Check(ctx context.Context, res restrict.Resource, opts ...rbac.CheckOption) (rbac.Subject, error) {
|
||||||
|
_va := make([]interface{}, len(opts))
|
||||||
|
for _i := range opts {
|
||||||
|
_va[_i] = opts[_i]
|
||||||
|
}
|
||||||
|
var _ca []interface{}
|
||||||
|
_ca = append(_ca, ctx, res)
|
||||||
|
_ca = append(_ca, _va...)
|
||||||
|
ret := _m.Called(_ca...)
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for Check")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 rbac.Subject
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, restrict.Resource, ...rbac.CheckOption) (rbac.Subject, error)); ok {
|
||||||
|
return rf(ctx, res, opts...)
|
||||||
|
}
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, restrict.Resource, ...rbac.CheckOption) rbac.Subject); ok {
|
||||||
|
r0 = rf(ctx, res, opts...)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(rbac.Subject)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, restrict.Resource, ...rbac.CheckOption) error); ok {
|
||||||
|
r1 = rf(ctx, res, opts...)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// RBAC_Check_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Check'
|
||||||
|
type RBAC_Check_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check is a helper method to define mock.On call
|
||||||
|
// - ctx context.Context
|
||||||
|
// - res restrict.Resource
|
||||||
|
// - opts ...rbac.CheckOption
|
||||||
|
func (_e *RBAC_Expecter) Check(ctx interface{}, res interface{}, opts ...interface{}) *RBAC_Check_Call {
|
||||||
|
return &RBAC_Check_Call{Call: _e.mock.On("Check",
|
||||||
|
append([]interface{}{ctx, res}, opts...)...)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *RBAC_Check_Call) Run(run func(ctx context.Context, res restrict.Resource, opts ...rbac.CheckOption)) *RBAC_Check_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
variadicArgs := make([]rbac.CheckOption, len(args)-2)
|
||||||
|
for i, a := range args[2:] {
|
||||||
|
if a != nil {
|
||||||
|
variadicArgs[i] = a.(rbac.CheckOption)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run(args[0].(context.Context), args[1].(restrict.Resource), variadicArgs...)
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *RBAC_Check_Call) Return(_a0 rbac.Subject, _a1 error) *RBAC_Check_Call {
|
||||||
|
_c.Call.Return(_a0, _a1)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *RBAC_Check_Call) RunAndReturn(run func(context.Context, restrict.Resource, ...rbac.CheckOption) (rbac.Subject, error)) *RBAC_Check_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRBAC creates a new instance of RBAC. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||||
|
// The first argument is typically a *testing.T value.
|
||||||
|
func NewRBAC(t interface {
|
||||||
|
mock.TestingT
|
||||||
|
Cleanup(func())
|
||||||
|
}) *RBAC {
|
||||||
|
mock := &RBAC{}
|
||||||
|
mock.Mock.Test(t)
|
||||||
|
|
||||||
|
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||||
|
|
||||||
|
return mock
|
||||||
|
}
|
421
pkg/rbac/rbac.go
Normal file
421
pkg/rbac/rbac.go
Normal file
|
@ -0,0 +1,421 @@
|
||||||
|
package rbac
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/el-mike/restrict/v2"
|
||||||
|
"github.com/el-mike/restrict/v2/adapters"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
RoleUser = "User"
|
||||||
|
RoleSubmitter = "Submitter"
|
||||||
|
RoleAdmin = "Admin"
|
||||||
|
RoleSystem = "System"
|
||||||
|
RolePublic = "Public"
|
||||||
|
RoleShareGuest = "ShareGuest"
|
||||||
|
|
||||||
|
ResourceCall = "Call"
|
||||||
|
ResourceIncident = "Incident"
|
||||||
|
ResourceTalkgroup = "Talkgroup"
|
||||||
|
ResourceAlert = "Alert"
|
||||||
|
ResourceShare = "Share"
|
||||||
|
ResourceAPIKey = "APIKey"
|
||||||
|
|
||||||
|
ActionRead = "read"
|
||||||
|
ActionCreate = "create"
|
||||||
|
ActionUpdate = "update"
|
||||||
|
ActionDelete = "delete"
|
||||||
|
|
||||||
|
PresetUpdateOwn = "updateOwn"
|
||||||
|
PresetDeleteOwn = "deleteOwn"
|
||||||
|
PresetReadShared = "readShared"
|
||||||
|
|
||||||
|
PresetUpdateSubmitter = "updateSubmitter"
|
||||||
|
PresetDeleteSubmitter = "deleteSubmitter"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrBadSubject = errors.New("bad subject in token")
|
||||||
|
)
|
||||||
|
|
||||||
|
type subjectContextKey string
|
||||||
|
|
||||||
|
const SubjectCtxKey subjectContextKey = "sub"
|
||||||
|
|
||||||
|
func CtxWithSubject(ctx context.Context, sub Subject) context.Context {
|
||||||
|
return context.WithValue(ctx, SubjectCtxKey, sub)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ErrAccessDenied(err error) *restrict.AccessDeniedError {
|
||||||
|
if accessErr, ok := err.(*restrict.AccessDeniedError); ok {
|
||||||
|
return accessErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SubjectFrom(ctx context.Context) Subject {
|
||||||
|
sub, ok := ctx.Value(SubjectCtxKey).(Subject)
|
||||||
|
if ok {
|
||||||
|
return sub
|
||||||
|
}
|
||||||
|
|
||||||
|
return new(PublicSubject)
|
||||||
|
}
|
||||||
|
|
||||||
|
type rbacCtxKey string
|
||||||
|
|
||||||
|
const RBACCtxKey rbacCtxKey = "rbac"
|
||||||
|
|
||||||
|
func FromCtx(ctx context.Context) RBAC {
|
||||||
|
rbac, ok := ctx.Value(RBACCtxKey).(RBAC)
|
||||||
|
if !ok {
|
||||||
|
panic("no RBAC in context")
|
||||||
|
}
|
||||||
|
|
||||||
|
return rbac
|
||||||
|
}
|
||||||
|
|
||||||
|
func CtxWithRBAC(ctx context.Context, rbac RBAC) context.Context {
|
||||||
|
return context.WithValue(ctx, RBACCtxKey, rbac)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNotAuthorized = errors.New("not authorized")
|
||||||
|
)
|
||||||
|
|
||||||
|
var policy = &restrict.PolicyDefinition{
|
||||||
|
Roles: restrict.Roles{
|
||||||
|
RoleUser: {
|
||||||
|
Description: "An authenticated user",
|
||||||
|
Grants: restrict.GrantsMap{
|
||||||
|
ResourceIncident: {
|
||||||
|
&restrict.Permission{Action: ActionRead},
|
||||||
|
&restrict.Permission{Action: ActionCreate},
|
||||||
|
&restrict.Permission{Preset: PresetUpdateOwn},
|
||||||
|
&restrict.Permission{Preset: PresetDeleteOwn},
|
||||||
|
},
|
||||||
|
ResourceCall: {
|
||||||
|
&restrict.Permission{Action: ActionRead},
|
||||||
|
&restrict.Permission{Action: ActionCreate},
|
||||||
|
&restrict.Permission{Preset: PresetUpdateSubmitter},
|
||||||
|
&restrict.Permission{Preset: PresetDeleteSubmitter},
|
||||||
|
},
|
||||||
|
ResourceTalkgroup: {
|
||||||
|
&restrict.Permission{Action: ActionRead},
|
||||||
|
},
|
||||||
|
ResourceShare: {
|
||||||
|
&restrict.Permission{Action: ActionRead},
|
||||||
|
&restrict.Permission{Action: ActionCreate},
|
||||||
|
&restrict.Permission{Preset: PresetUpdateOwn},
|
||||||
|
&restrict.Permission{Preset: PresetDeleteOwn},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RoleSubmitter: {
|
||||||
|
Description: "A role that can submit calls",
|
||||||
|
Grants: restrict.GrantsMap{
|
||||||
|
ResourceCall: {
|
||||||
|
&restrict.Permission{Action: ActionCreate},
|
||||||
|
},
|
||||||
|
ResourceTalkgroup: {
|
||||||
|
// for learning TGs
|
||||||
|
&restrict.Permission{Action: ActionCreate},
|
||||||
|
&restrict.Permission{Action: ActionUpdate},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RoleShareGuest: {
|
||||||
|
Description: "Someone who has a valid share link",
|
||||||
|
Grants: restrict.GrantsMap{
|
||||||
|
ResourceCall: {
|
||||||
|
&restrict.Permission{Preset: PresetReadShared},
|
||||||
|
},
|
||||||
|
ResourceIncident: {
|
||||||
|
&restrict.Permission{Preset: PresetReadShared},
|
||||||
|
},
|
||||||
|
ResourceTalkgroup: {
|
||||||
|
&restrict.Permission{Action: ActionRead},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RoleAdmin: {
|
||||||
|
Parents: []string{RoleUser},
|
||||||
|
Grants: restrict.GrantsMap{
|
||||||
|
ResourceIncident: {
|
||||||
|
&restrict.Permission{Action: ActionUpdate},
|
||||||
|
&restrict.Permission{Action: ActionDelete},
|
||||||
|
},
|
||||||
|
ResourceCall: {
|
||||||
|
&restrict.Permission{Action: ActionUpdate},
|
||||||
|
&restrict.Permission{Action: ActionDelete},
|
||||||
|
},
|
||||||
|
ResourceTalkgroup: {
|
||||||
|
&restrict.Permission{Action: ActionUpdate},
|
||||||
|
&restrict.Permission{Action: ActionCreate},
|
||||||
|
&restrict.Permission{Action: ActionDelete},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RoleSystem: {
|
||||||
|
Parents: []string{RoleSystem},
|
||||||
|
},
|
||||||
|
RolePublic: {
|
||||||
|
/*
|
||||||
|
Grants: restrict.GrantsMap{
|
||||||
|
ResourceShare: {
|
||||||
|
&restrict.Permission{Action: ActionRead},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PermissionPresets: restrict.PermissionPresets{
|
||||||
|
PresetUpdateOwn: &restrict.Permission{
|
||||||
|
Action: ActionUpdate,
|
||||||
|
Conditions: restrict.Conditions{
|
||||||
|
&restrict.EqualCondition{
|
||||||
|
ID: "isOwner",
|
||||||
|
Left: &restrict.ValueDescriptor{
|
||||||
|
Source: restrict.ResourceField,
|
||||||
|
Field: "Owner",
|
||||||
|
},
|
||||||
|
Right: &restrict.ValueDescriptor{
|
||||||
|
Source: restrict.SubjectField,
|
||||||
|
Field: "ID",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PresetDeleteOwn: &restrict.Permission{
|
||||||
|
Action: ActionDelete,
|
||||||
|
Conditions: restrict.Conditions{
|
||||||
|
&restrict.EqualCondition{
|
||||||
|
ID: "isOwner",
|
||||||
|
Left: &restrict.ValueDescriptor{
|
||||||
|
Source: restrict.ResourceField,
|
||||||
|
Field: "Owner",
|
||||||
|
},
|
||||||
|
Right: &restrict.ValueDescriptor{
|
||||||
|
Source: restrict.SubjectField,
|
||||||
|
Field: "ID",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PresetUpdateSubmitter: &restrict.Permission{
|
||||||
|
Action: ActionUpdate,
|
||||||
|
Conditions: restrict.Conditions{
|
||||||
|
&SubmitterEqualCondition{
|
||||||
|
ID: "isSubmitter",
|
||||||
|
Left: &restrict.ValueDescriptor{
|
||||||
|
Source: restrict.ResourceField,
|
||||||
|
Field: "Submitter",
|
||||||
|
},
|
||||||
|
Right: &restrict.ValueDescriptor{
|
||||||
|
Source: restrict.SubjectField,
|
||||||
|
Field: "ID",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PresetDeleteSubmitter: &restrict.Permission{
|
||||||
|
Action: ActionDelete,
|
||||||
|
Conditions: restrict.Conditions{
|
||||||
|
&SubmitterEqualCondition{
|
||||||
|
ID: "isSubmitter",
|
||||||
|
Left: &restrict.ValueDescriptor{
|
||||||
|
Source: restrict.ResourceField,
|
||||||
|
Field: "Submitter",
|
||||||
|
},
|
||||||
|
Right: &restrict.ValueDescriptor{
|
||||||
|
Source: restrict.SubjectField,
|
||||||
|
Field: "ID",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PresetReadShared: &restrict.Permission{
|
||||||
|
Action: ActionRead,
|
||||||
|
Conditions: restrict.Conditions{
|
||||||
|
&restrict.EqualCondition{
|
||||||
|
ID: "isOwner",
|
||||||
|
Left: &restrict.ValueDescriptor{
|
||||||
|
Source: restrict.ContextField,
|
||||||
|
Field: "Owner",
|
||||||
|
},
|
||||||
|
Right: &restrict.ValueDescriptor{
|
||||||
|
Source: restrict.SubjectField,
|
||||||
|
Field: "ID",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type checkOptions struct {
|
||||||
|
actions []string
|
||||||
|
context restrict.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
type CheckOption func(*checkOptions)
|
||||||
|
|
||||||
|
func WithActions(actions ...string) CheckOption {
|
||||||
|
return func(o *checkOptions) {
|
||||||
|
o.actions = append(o.actions, actions...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithContext(ctx restrict.Context) CheckOption {
|
||||||
|
return func(o *checkOptions) {
|
||||||
|
o.context = ctx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func UseResource(rsc string) restrict.Resource {
|
||||||
|
return restrict.UseResource(rsc)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Subject interface {
|
||||||
|
restrict.Subject
|
||||||
|
GetName() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Resource interface {
|
||||||
|
restrict.Resource
|
||||||
|
}
|
||||||
|
|
||||||
|
type RBAC interface {
|
||||||
|
Check(ctx context.Context, res restrict.Resource, opts ...CheckOption) (Subject, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type rbac struct {
|
||||||
|
policy *restrict.PolicyManager
|
||||||
|
access *restrict.AccessManager
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() (*rbac, error) {
|
||||||
|
adapter := adapters.NewInMemoryAdapter(policy)
|
||||||
|
polMan, err := restrict.NewPolicyManager(adapter, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
accMan := restrict.NewAccessManager(polMan)
|
||||||
|
return &rbac{
|
||||||
|
policy: polMan,
|
||||||
|
access: accMan,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check is a convenience function to pull the RBAC instance out of ctx and Check.
|
||||||
|
func Check(ctx context.Context, res restrict.Resource, opts ...CheckOption) (Subject, error) {
|
||||||
|
return FromCtx(ctx).Check(ctx, res, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *rbac) Check(ctx context.Context, res restrict.Resource, opts ...CheckOption) (Subject, error) {
|
||||||
|
sub := SubjectFrom(ctx)
|
||||||
|
o := checkOptions{}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(&o)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &restrict.AccessRequest{
|
||||||
|
Subject: sub,
|
||||||
|
Resource: res,
|
||||||
|
Actions: o.actions,
|
||||||
|
Context: o.context,
|
||||||
|
}
|
||||||
|
|
||||||
|
return sub, r.access.Authorize(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShareLinkGuest struct {
|
||||||
|
ShareID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ShareLinkGuest) GetName() string {
|
||||||
|
return "SHARE:" + s.ShareID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ShareLinkGuest) GetRoles() []string {
|
||||||
|
return []string{RoleShareGuest}
|
||||||
|
}
|
||||||
|
|
||||||
|
type PublicSubject struct {
|
||||||
|
RemoteAddr string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PublicSubject) GetName() string {
|
||||||
|
return "PUBLIC:" + s.RemoteAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PublicSubject) GetRoles() []string {
|
||||||
|
return []string{RolePublic}
|
||||||
|
}
|
||||||
|
|
||||||
|
type SystemServiceSubject struct {
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SystemServiceSubject) GetName() string {
|
||||||
|
return "SYSTEM:" + s.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SystemServiceSubject) GetRoles() []string {
|
||||||
|
return []string{RoleSystem}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
SubmitterEqualConditionType = "SUBMITTER_EQUAL"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SubmitterEqualCondition struct {
|
||||||
|
ID string `json:"name,omitempty" yaml:"name,omitempty"`
|
||||||
|
Left *restrict.ValueDescriptor `json:"left" yaml:"left"`
|
||||||
|
Right *restrict.ValueDescriptor `json:"right" yaml:"right"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SubmitterEqualCondition) Type() string {
|
||||||
|
return SubmitterEqualConditionType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SubmitterEqualCondition) Check(r *restrict.AccessRequest) error {
|
||||||
|
left, err := c.Left.GetValue(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
right, err := c.Right.GetValue(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
lVal := reflect.ValueOf(left)
|
||||||
|
rVal := reflect.ValueOf(right)
|
||||||
|
|
||||||
|
// deref Left. this is the difference between us and EqualCondition
|
||||||
|
for lVal.Kind() == reflect.Pointer {
|
||||||
|
lVal = lVal.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !lVal.IsValid() || !reflect.DeepEqual(rVal.Interface(), lVal.Interface()) {
|
||||||
|
return restrict.NewConditionNotSatisfiedError(c, r, fmt.Errorf("values \"%v\" and \"%v\" are not equal", left, right))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SubmitterEqualConditionFactory() restrict.Condition {
|
||||||
|
return new(SubmitterEqualCondition)
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
restrict.RegisterConditionFactory(SubmitterEqualConditionType, SubmitterEqualConditionFactory)
|
||||||
|
}
|
197
pkg/rbac/rbac_test.go
Normal file
197
pkg/rbac/rbac_test.go
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
package rbac_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"dynatron.me/x/stillbox/internal/common"
|
||||||
|
"dynatron.me/x/stillbox/pkg/calls"
|
||||||
|
"dynatron.me/x/stillbox/pkg/incidents"
|
||||||
|
"dynatron.me/x/stillbox/pkg/rbac"
|
||||||
|
"dynatron.me/x/stillbox/pkg/talkgroups"
|
||||||
|
"dynatron.me/x/stillbox/pkg/users"
|
||||||
|
"github.com/el-mike/restrict/v2"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRBAC(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
subject rbac.Subject
|
||||||
|
resource rbac.Resource
|
||||||
|
action string
|
||||||
|
expectErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "admin update talkgroup",
|
||||||
|
subject: &users.User{
|
||||||
|
ID: 2,
|
||||||
|
IsAdmin: true,
|
||||||
|
},
|
||||||
|
resource: &talkgroups.Talkgroup{},
|
||||||
|
action: rbac.ActionUpdate,
|
||||||
|
expectErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "admin update incident",
|
||||||
|
subject: &users.User{
|
||||||
|
ID: 2,
|
||||||
|
IsAdmin: true,
|
||||||
|
},
|
||||||
|
resource: &incidents.Incident{
|
||||||
|
Name: "test incident",
|
||||||
|
Owner: 4,
|
||||||
|
},
|
||||||
|
action: rbac.ActionUpdate,
|
||||||
|
expectErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "user update incident not owner",
|
||||||
|
subject: &users.User{
|
||||||
|
ID: 2,
|
||||||
|
},
|
||||||
|
resource: &incidents.Incident{
|
||||||
|
Name: "test incident",
|
||||||
|
Owner: 4,
|
||||||
|
},
|
||||||
|
action: rbac.ActionUpdate,
|
||||||
|
expectErr: errors.New(`access denied for Action: "update" on Resource: "Incident"`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "user update incident owner",
|
||||||
|
subject: &users.User{
|
||||||
|
ID: 2,
|
||||||
|
},
|
||||||
|
resource: &incidents.Incident{
|
||||||
|
Name: "test incident",
|
||||||
|
Owner: 2,
|
||||||
|
},
|
||||||
|
action: rbac.ActionUpdate,
|
||||||
|
expectErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "user delete incident not owner",
|
||||||
|
subject: &users.User{
|
||||||
|
ID: 2,
|
||||||
|
},
|
||||||
|
resource: &incidents.Incident{
|
||||||
|
Name: "test incident",
|
||||||
|
Owner: 6,
|
||||||
|
},
|
||||||
|
action: rbac.ActionDelete,
|
||||||
|
expectErr: errors.New(`access denied for Action: "delete" on Resource: "Incident"`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "admin update call",
|
||||||
|
subject: &users.User{
|
||||||
|
ID: 2,
|
||||||
|
IsAdmin: true,
|
||||||
|
},
|
||||||
|
resource: &calls.Call{
|
||||||
|
Submitter: common.PtrTo(users.UserID(4)),
|
||||||
|
},
|
||||||
|
action: rbac.ActionUpdate,
|
||||||
|
expectErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "user update call not owner",
|
||||||
|
subject: &users.User{
|
||||||
|
ID: 2,
|
||||||
|
},
|
||||||
|
resource: &calls.Call{
|
||||||
|
Submitter: common.PtrTo(users.UserID(4)),
|
||||||
|
},
|
||||||
|
action: rbac.ActionUpdate,
|
||||||
|
expectErr: errors.New(`access denied for Action: "update" on Resource: "Call"`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "user update call owner",
|
||||||
|
subject: &users.User{
|
||||||
|
ID: 2,
|
||||||
|
},
|
||||||
|
resource: &calls.Call{
|
||||||
|
Submitter: common.PtrTo(users.UserID(2)),
|
||||||
|
},
|
||||||
|
action: rbac.ActionUpdate,
|
||||||
|
expectErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "user update call nil submitter",
|
||||||
|
subject: &users.User{
|
||||||
|
ID: 2,
|
||||||
|
},
|
||||||
|
resource: &calls.Call{
|
||||||
|
Submitter: nil,
|
||||||
|
},
|
||||||
|
action: rbac.ActionUpdate,
|
||||||
|
expectErr: errors.New(`access denied for Action: "update" on Resource: "Call"`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "user delete call not owner",
|
||||||
|
subject: &users.User{
|
||||||
|
ID: 2,
|
||||||
|
},
|
||||||
|
resource: &calls.Call{
|
||||||
|
Submitter: common.PtrTo(users.UserID(6)),
|
||||||
|
},
|
||||||
|
action: rbac.ActionDelete,
|
||||||
|
expectErr: errors.New(`access denied for Action: "delete" on Resource: "Call"`),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
ctx := rbac.CtxWithSubject(context.Background(), tc.subject)
|
||||||
|
rb, err := rbac.New()
|
||||||
|
require.NoError(t, err)
|
||||||
|
sub, err := rb.Check(ctx, tc.resource, rbac.WithActions(tc.action))
|
||||||
|
if tc.expectErr != nil {
|
||||||
|
assert.Equal(t, tc.expectErr.Error(), err.Error())
|
||||||
|
} else {
|
||||||
|
if !assert.NoError(t, err) {
|
||||||
|
accErr(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.Equal(t, tc.subject, sub)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func accErr(err error) {
|
||||||
|
if accessError, ok := err.(*restrict.AccessDeniedError); ok {
|
||||||
|
// Error() implementation. Returns a message in a form: "access denied for Action/s: ... on Resource: ..."
|
||||||
|
fmt.Println(accessError)
|
||||||
|
// Returns an AccessRequest that failed.
|
||||||
|
fmt.Println(accessError.Request)
|
||||||
|
// Returns first reason for the denied access.
|
||||||
|
// Especially helpful in fail-early mode, where there will only be one Reason.
|
||||||
|
fmt.Println(accessError.FirstReason())
|
||||||
|
|
||||||
|
// Reasons property will hold all errors that caused the access to be denied.
|
||||||
|
for _, permissionErr := range accessError.Reasons {
|
||||||
|
fmt.Println(permissionErr)
|
||||||
|
fmt.Println(permissionErr.Action)
|
||||||
|
fmt.Println(permissionErr.RoleName)
|
||||||
|
fmt.Println(permissionErr.ResourceName)
|
||||||
|
|
||||||
|
// Returns first ConditionNotSatisfied error for given PermissionError, if any was returned for given PermissionError.
|
||||||
|
// Especially helpful in fail-early mode, where there will only be one failed Condition.
|
||||||
|
fmt.Println(permissionErr.FirstConditionError())
|
||||||
|
|
||||||
|
// ConditionErrors property will hold all ConditionNotSatisfied errors.
|
||||||
|
for _, conditionErr := range permissionErr.ConditionErrors {
|
||||||
|
fmt.Println(conditionErr)
|
||||||
|
fmt.Println(conditionErr.Reason)
|
||||||
|
|
||||||
|
// Every ConditionNotSatisfied contains an instance of Condition that returned it,
|
||||||
|
// so it can be tested using type assertion to get more details about failed Condition.
|
||||||
|
if emptyCondition, ok := conditionErr.Condition.(*restrict.EmptyCondition); ok {
|
||||||
|
fmt.Println(emptyCondition.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"dynatron.me/x/stillbox/internal/common"
|
"dynatron.me/x/stillbox/internal/common"
|
||||||
|
"dynatron.me/x/stillbox/pkg/rbac"
|
||||||
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
|
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
@ -37,6 +38,7 @@ func (a *api) Subrouter() http.Handler {
|
||||||
r.Mount("/call", new(callsAPI).Subrouter())
|
r.Mount("/call", new(callsAPI).Subrouter())
|
||||||
r.Mount("/user", new(usersAPI).Subrouter())
|
r.Mount("/user", new(usersAPI).Subrouter())
|
||||||
r.Mount("/incident", newIncidentsAPI(&a.baseURL).Subrouter())
|
r.Mount("/incident", newIncidentsAPI(&a.baseURL).Subrouter())
|
||||||
|
r.Mount("/share", newShareHandler(&a.baseURL).Subrouter())
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
@ -82,6 +84,14 @@ func unauthErrText(err error) render.Renderer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func forbiddenErrText(err error) render.Renderer {
|
||||||
|
return &errResponse{
|
||||||
|
Err: err,
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
Error: "Forbidden: " + err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func constraintErrText(err error) render.Renderer {
|
func constraintErrText(err error) render.Renderer {
|
||||||
return &errResponse{
|
return &errResponse{
|
||||||
Err: err,
|
Err: err,
|
||||||
|
@ -127,9 +137,10 @@ var statusMapping = map[error]errResponder{
|
||||||
ErrTGIDMismatch: badRequestErrText,
|
ErrTGIDMismatch: badRequestErrText,
|
||||||
ErrSysMismatch: badRequestErrText,
|
ErrSysMismatch: badRequestErrText,
|
||||||
tgstore.ErrReference: constraintErrText,
|
tgstore.ErrReference: constraintErrText,
|
||||||
ErrBadUID: unauthErrText,
|
rbac.ErrBadSubject: unauthErrText,
|
||||||
ErrBadAppName: unauthErrText,
|
ErrBadAppName: unauthErrText,
|
||||||
common.ErrPageOutOfRange: badRequestErrText,
|
common.ErrPageOutOfRange: badRequestErrText,
|
||||||
|
rbac.ErrNotAuthorized: unauthErrText,
|
||||||
}
|
}
|
||||||
|
|
||||||
func autoError(err error) render.Renderer {
|
func autoError(err error) render.Renderer {
|
||||||
|
@ -144,6 +155,10 @@ func autoError(err error) render.Renderer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if rbac.ErrAccessDenied(err) != nil {
|
||||||
|
return forbiddenErrText(err)
|
||||||
|
}
|
||||||
|
|
||||||
return internalError(err)
|
return internalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
231
pkg/rest/share.go
Normal file
231
pkg/rest/share.go
Normal file
|
@ -0,0 +1,231 @@
|
||||||
|
package rest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"dynatron.me/x/stillbox/internal/common"
|
||||||
|
"dynatron.me/x/stillbox/internal/forms"
|
||||||
|
"dynatron.me/x/stillbox/internal/jsontypes"
|
||||||
|
"dynatron.me/x/stillbox/pkg/incidents"
|
||||||
|
"dynatron.me/x/stillbox/pkg/incidents/incstore"
|
||||||
|
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type shareAPI struct {
|
||||||
|
baseURL *url.URL
|
||||||
|
}
|
||||||
|
|
||||||
|
func newShareHandler(baseURL *url.URL) API {
|
||||||
|
return &shareAPI{baseURL}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ia *shareAPI) Subrouter() http.Handler {
|
||||||
|
r := chi.NewMux()
|
||||||
|
|
||||||
|
//r.Get(`/{id:[A-Za-z0-9_-]{20,}}`, ia.getShare)
|
||||||
|
//r.Post('/create', ia.createShare)
|
||||||
|
//r.Delete(`/{id:[A-Za-z0-9_-]{20,}}`, ia.deleteShare)
|
||||||
|
//r.Get(`/`, ia.getShares)
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ia *shareAPI) listIncidents(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
incs := incstore.FromCtx(ctx)
|
||||||
|
|
||||||
|
p := incstore.IncidentsParams{}
|
||||||
|
err := forms.Unmarshal(r, &p, forms.WithTag("json"), forms.WithAcceptBlank(), forms.WithOmitEmpty())
|
||||||
|
if err != nil {
|
||||||
|
wErr(w, r, badRequest(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res := struct {
|
||||||
|
Incidents []incstore.Incident `json:"incidents"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
res.Incidents, res.Count, err = incs.Incidents(ctx, p)
|
||||||
|
if err != nil {
|
||||||
|
wErr(w, r, autoError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respond(w, r, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ia *shareAPI) createIncident(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
incs := incstore.FromCtx(ctx)
|
||||||
|
|
||||||
|
p := incidents.Incident{}
|
||||||
|
err := forms.Unmarshal(r, &p, forms.WithTag("json"), forms.WithAcceptBlank(), forms.WithOmitEmpty())
|
||||||
|
if err != nil {
|
||||||
|
wErr(w, r, badRequest(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
inc, err := incs.CreateIncident(ctx, p)
|
||||||
|
if err != nil {
|
||||||
|
wErr(w, r, autoError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respond(w, r, inc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ia *shareAPI) getIncident(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
incs := incstore.FromCtx(ctx)
|
||||||
|
|
||||||
|
id, err := idOnlyParam(w, r)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
inc, err := incs.Incident(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
wErr(w, r, autoError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respond(w, r, inc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ia *shareAPI) updateIncident(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
incs := incstore.FromCtx(ctx)
|
||||||
|
|
||||||
|
id, err := idOnlyParam(w, r)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
p := incstore.UpdateIncidentParams{}
|
||||||
|
err = forms.Unmarshal(r, &p, forms.WithTag("json"), forms.WithAcceptBlank(), forms.WithOmitEmpty())
|
||||||
|
if err != nil {
|
||||||
|
wErr(w, r, badRequest(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
inc, err := incs.UpdateIncident(ctx, id, p)
|
||||||
|
if err != nil {
|
||||||
|
wErr(w, r, autoError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respond(w, r, inc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ia *shareAPI) deleteIncident(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
incs := incstore.FromCtx(ctx)
|
||||||
|
|
||||||
|
urlParams := struct {
|
||||||
|
ID uuid.UUID `param:"id"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
err := decodeParams(&urlParams, r)
|
||||||
|
if err != nil {
|
||||||
|
wErr(w, r, badRequest(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = incs.DeleteIncident(ctx, urlParams.ID)
|
||||||
|
if err != nil {
|
||||||
|
wErr(w, r, autoError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CallIncidentParams2 struct {
|
||||||
|
Add jsontypes.UUIDs `json:"add"`
|
||||||
|
Notes json.RawMessage `json:"notes"`
|
||||||
|
|
||||||
|
Remove jsontypes.UUIDs `json:"remove"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ia *shareAPI) postCalls(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
incs := incstore.FromCtx(ctx)
|
||||||
|
|
||||||
|
id, err := idOnlyParam(w, r)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
p := CallIncidentParams2{}
|
||||||
|
err = forms.Unmarshal(r, &p, forms.WithTag("json"), forms.WithAcceptBlank(), forms.WithOmitEmpty())
|
||||||
|
if err != nil {
|
||||||
|
wErr(w, r, badRequest(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = incs.AddRemoveIncidentCalls(ctx, id, p.Add.UUIDs(), p.Notes, p.Remove.UUIDs())
|
||||||
|
if err != nil {
|
||||||
|
wErr(w, r, autoError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ia *shareAPI) getCallsM3U(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
incs := incstore.FromCtx(ctx)
|
||||||
|
tgst := tgstore.FromCtx(ctx)
|
||||||
|
|
||||||
|
id, err := idOnlyParam(w, r)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
inc, err := incs.Incident(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
wErr(w, r, autoError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b := new(bytes.Buffer)
|
||||||
|
|
||||||
|
callUrl := common.PtrTo(*ia.baseURL)
|
||||||
|
|
||||||
|
b.WriteString("#EXTM3U\n\n")
|
||||||
|
for _, c := range inc.Calls {
|
||||||
|
tg, err := tgst.TG(ctx, c.TalkgroupTuple())
|
||||||
|
if err != nil {
|
||||||
|
wErr(w, r, autoError(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var from string
|
||||||
|
if c.Source != 0 {
|
||||||
|
from = fmt.Sprintf(" from %d", c.Source)
|
||||||
|
}
|
||||||
|
|
||||||
|
callUrl.Path = "/api/call/" + c.ID.String()
|
||||||
|
|
||||||
|
fmt.Fprintf(b, "#EXTINF:%d,%s%s (%s)\n%s\n\n",
|
||||||
|
c.Duration.Seconds(),
|
||||||
|
tg.StringTag(true),
|
||||||
|
from,
|
||||||
|
c.DateTime.Format("15:04 01/02"),
|
||||||
|
callUrl,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not a lot of agreement on which MIME type to use for non-HLS m3u,
|
||||||
|
// let's hope this is good enough
|
||||||
|
w.Header().Set("Content-Type", "audio/x-mpegurl")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = b.WriteTo(w)
|
||||||
|
}
|
|
@ -7,13 +7,13 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"dynatron.me/x/stillbox/pkg/auth"
|
"dynatron.me/x/stillbox/pkg/auth"
|
||||||
|
"dynatron.me/x/stillbox/pkg/rbac"
|
||||||
"dynatron.me/x/stillbox/pkg/users"
|
"dynatron.me/x/stillbox/pkg/users"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrBadUID = errors.New("bad UID in token")
|
|
||||||
ErrBadAppName = errors.New("bad app name")
|
ErrBadAppName = errors.New("bad app name")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -32,10 +32,10 @@ func (ua *usersAPI) Subrouter() http.Handler {
|
||||||
func (ua *usersAPI) getPrefs(w http.ResponseWriter, r *http.Request) {
|
func (ua *usersAPI) getPrefs(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
uid := auth.UIDFrom(ctx)
|
username := auth.UsernameFrom(ctx)
|
||||||
|
|
||||||
if uid == nil {
|
if username == nil {
|
||||||
wErr(w, r, autoError(ErrBadUID))
|
wErr(w, r, autoError(rbac.ErrBadSubject))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ func (ua *usersAPI) getPrefs(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
us := users.FromCtx(ctx)
|
us := users.FromCtx(ctx)
|
||||||
prefs, err := us.UserPrefs(ctx, *uid, *p.AppName)
|
prefs, err := us.UserPrefs(ctx, *username, *p.AppName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
wErr(w, r, autoError(err))
|
wErr(w, r, autoError(err))
|
||||||
return
|
return
|
||||||
|
@ -67,10 +67,10 @@ func (ua *usersAPI) getPrefs(w http.ResponseWriter, r *http.Request) {
|
||||||
func (ua *usersAPI) putPrefs(w http.ResponseWriter, r *http.Request) {
|
func (ua *usersAPI) putPrefs(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
uid := auth.UIDFrom(ctx)
|
username := auth.UsernameFrom(ctx)
|
||||||
|
|
||||||
if uid == nil {
|
if username == nil {
|
||||||
wErr(w, r, autoError(ErrBadUID))
|
wErr(w, r, autoError(rbac.ErrBadSubject))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,7 +102,7 @@ func (ua *usersAPI) putPrefs(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
us := users.FromCtx(ctx)
|
us := users.FromCtx(ctx)
|
||||||
err = us.SetUserPrefs(ctx, *uid, *p.AppName, prefs)
|
err = us.SetUserPrefs(ctx, *username, *p.AppName, prefs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
wErr(w, r, autoError(err))
|
wErr(w, r, autoError(err))
|
||||||
return
|
return
|
||||||
|
|
|
@ -7,5 +7,6 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) Ingest(ctx context.Context, call *calls.Call) error {
|
func (s *Server) Ingest(ctx context.Context, call *calls.Call) error {
|
||||||
return s.sinks.EmitCall(context.Background(), call)
|
ctx = context.WithoutCancel(ctx)
|
||||||
|
return s.sinks.EmitCall(ctx, call)
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,9 +47,11 @@ func (s *Server) setupRoutes() {
|
||||||
})
|
})
|
||||||
|
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
// auth routes get rate-limited heavily, but not using middleware
|
// auth/share routes get rate-limited heavily, but not using middleware
|
||||||
|
s.rateLimit(r)
|
||||||
r.Use(render.SetContentType(render.ContentTypeJSON))
|
r.Use(render.SetContentType(render.ContentTypeJSON))
|
||||||
s.auth.PublicRoutes(r)
|
s.auth.PublicRoutes(r)
|
||||||
|
// r.Mount("/share", s.share.ShareRouter(s.rest))
|
||||||
})
|
})
|
||||||
|
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
|
@ -66,7 +68,7 @@ func (s *Server) setupRoutes() {
|
||||||
func (s *Server) WithCtxStores() func(next http.Handler) http.Handler {
|
func (s *Server) WithCtxStores() func(next http.Handler) http.Handler {
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
r = r.WithContext(s.addStoresTo(r.Context()))
|
r = r.WithContext(s.fillCtx(r.Context()))
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
return http.HandlerFunc(fn)
|
return http.HandlerFunc(fn)
|
||||||
|
|
|
@ -15,7 +15,9 @@ import (
|
||||||
"dynatron.me/x/stillbox/pkg/incidents/incstore"
|
"dynatron.me/x/stillbox/pkg/incidents/incstore"
|
||||||
"dynatron.me/x/stillbox/pkg/nexus"
|
"dynatron.me/x/stillbox/pkg/nexus"
|
||||||
"dynatron.me/x/stillbox/pkg/notify"
|
"dynatron.me/x/stillbox/pkg/notify"
|
||||||
|
"dynatron.me/x/stillbox/pkg/rbac"
|
||||||
"dynatron.me/x/stillbox/pkg/rest"
|
"dynatron.me/x/stillbox/pkg/rest"
|
||||||
|
"dynatron.me/x/stillbox/pkg/share"
|
||||||
"dynatron.me/x/stillbox/pkg/sinks"
|
"dynatron.me/x/stillbox/pkg/sinks"
|
||||||
"dynatron.me/x/stillbox/pkg/sources"
|
"dynatron.me/x/stillbox/pkg/sources"
|
||||||
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
|
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
|
||||||
|
@ -48,6 +50,8 @@ type Server struct {
|
||||||
users users.Store
|
users users.Store
|
||||||
calls callstore.Store
|
calls callstore.Store
|
||||||
incidents incstore.Store
|
incidents incstore.Store
|
||||||
|
share share.Service
|
||||||
|
rbac rbac.RBAC
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(ctx context.Context, cfg *config.Configuration) (*Server, error) {
|
func New(ctx context.Context, cfg *config.Configuration) (*Server, error) {
|
||||||
|
@ -63,16 +67,23 @@ func New(ctx context.Context, cfg *config.Configuration) (*Server, error) {
|
||||||
|
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
|
|
||||||
authenticator := auth.NewAuthenticator(cfg.Auth)
|
ust := users.NewStore(db)
|
||||||
|
|
||||||
|
authenticator := auth.NewAuthenticator(cfg.Auth, ust)
|
||||||
|
|
||||||
notifier, err := notify.New(cfg.Notify)
|
notifier, err := notify.New(cfg.Notify)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
tgCache := tgstore.NewCache()
|
tgCache := tgstore.NewCache(db)
|
||||||
api := rest.New(cfg.BaseURL.URL())
|
api := rest.New(cfg.BaseURL.URL())
|
||||||
|
|
||||||
|
rbacSvc, err := rbac.New()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
srv := &Server{
|
srv := &Server{
|
||||||
auth: authenticator,
|
auth: authenticator,
|
||||||
conf: cfg,
|
conf: cfg,
|
||||||
|
@ -85,9 +96,11 @@ func New(ctx context.Context, cfg *config.Configuration) (*Server, error) {
|
||||||
tgs: tgCache,
|
tgs: tgCache,
|
||||||
sinks: sinks.NewSinkManager(),
|
sinks: sinks.NewSinkManager(),
|
||||||
rest: api,
|
rest: api,
|
||||||
users: users.NewStore(),
|
share: share.NewService(),
|
||||||
calls: callstore.NewStore(),
|
users: ust,
|
||||||
|
calls: callstore.NewStore(db),
|
||||||
incidents: incstore.NewStore(),
|
incidents: incstore.NewStore(),
|
||||||
|
rbac: rbacSvc,
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.DB.Partition.Enabled {
|
if cfg.DB.Partition.Enabled {
|
||||||
|
@ -102,7 +115,7 @@ func New(ctx context.Context, cfg *config.Configuration) (*Server, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
srv.sinks.Register("database", sinks.NewDatabaseSink(srv.db, tgCache), true)
|
srv.sinks.Register("database", sinks.NewDatabaseSink(db, tgCache), true)
|
||||||
srv.sinks.Register("nexus", sinks.NewNexusSink(srv.nex), false)
|
srv.sinks.Register("nexus", sinks.NewNexusSink(srv.nex), false)
|
||||||
|
|
||||||
if srv.alerter.Enabled() {
|
if srv.alerter.Enabled() {
|
||||||
|
@ -135,12 +148,14 @@ func New(ctx context.Context, cfg *config.Configuration) (*Server, error) {
|
||||||
return srv, nil
|
return srv, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) addStoresTo(ctx context.Context) context.Context {
|
func (s *Server) fillCtx(ctx context.Context) context.Context {
|
||||||
ctx = database.CtxWithDB(ctx, s.db)
|
ctx = database.CtxWithDB(ctx, s.db)
|
||||||
ctx = tgstore.CtxWithStore(ctx, s.tgs)
|
ctx = tgstore.CtxWithStore(ctx, s.tgs)
|
||||||
ctx = users.CtxWithStore(ctx, s.users)
|
ctx = users.CtxWithStore(ctx, s.users)
|
||||||
ctx = callstore.CtxWithStore(ctx, s.calls)
|
ctx = callstore.CtxWithStore(ctx, s.calls)
|
||||||
ctx = incstore.CtxWithStore(ctx, s.incidents)
|
ctx = incstore.CtxWithStore(ctx, s.incidents)
|
||||||
|
ctx = share.CtxWithStore(ctx, s.share.ShareStore())
|
||||||
|
ctx = rbac.CtxWithRBAC(ctx, s.rbac)
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
}
|
}
|
||||||
|
@ -150,7 +165,7 @@ func (s *Server) Go(ctx context.Context) error {
|
||||||
|
|
||||||
s.installHupHandler()
|
s.installHupHandler()
|
||||||
|
|
||||||
ctx = s.addStoresTo(ctx)
|
ctx = s.fillCtx(ctx)
|
||||||
|
|
||||||
httpSrv := &http.Server{
|
httpSrv := &http.Server{
|
||||||
Addr: s.conf.Listen,
|
Addr: s.conf.Listen,
|
||||||
|
@ -159,6 +174,7 @@ func (s *Server) Go(ctx context.Context) error {
|
||||||
|
|
||||||
go s.nex.Go(ctx)
|
go s.nex.Go(ctx)
|
||||||
go s.alerter.Go(ctx)
|
go s.alerter.Go(ctx)
|
||||||
|
go s.share.Go(ctx)
|
||||||
|
|
||||||
if pm := s.partman; pm != nil {
|
if pm := s.partman; pm != nil {
|
||||||
go pm.Go(ctx)
|
go pm.Go(ctx)
|
||||||
|
|
52
pkg/share/service.go
Normal file
52
pkg/share/service.go
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
package share
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"dynatron.me/x/stillbox/pkg/rbac"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PruneInterval = time.Hour * 4
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
ShareStore() Store
|
||||||
|
|
||||||
|
Go(ctx context.Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
Store
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) ShareStore() Store {
|
||||||
|
return s.Store
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) Go(ctx context.Context) {
|
||||||
|
ctx = rbac.CtxWithSubject(ctx, &rbac.SystemServiceSubject{Name: "share"})
|
||||||
|
|
||||||
|
tick := time.NewTicker(PruneInterval)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-tick.C:
|
||||||
|
err := s.Prune(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("share prune failed")
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
tick.Stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService() *service {
|
||||||
|
return &service{
|
||||||
|
Store: NewStore(),
|
||||||
|
}
|
||||||
|
}
|
61
pkg/share/share.go
Normal file
61
pkg/share/share.go
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
package share
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"dynatron.me/x/stillbox/internal/jsontypes"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/matoous/go-nanoid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SlugLength = 20
|
||||||
|
)
|
||||||
|
|
||||||
|
type EntityType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
EntityIncident EntityType = "incident"
|
||||||
|
EntityCall EntityType = "call"
|
||||||
|
)
|
||||||
|
|
||||||
|
// If an incident is shared, all calls that are part of it must be shared too, but this can be through the incident share (/share/bLaH/callID[.mp3])
|
||||||
|
|
||||||
|
type Share struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type EntityType `json:"entityType"`
|
||||||
|
EntityID uuid.UUID `json:"entityID"`
|
||||||
|
Expiration *jsontypes.Time `json:"expiration"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewShare creates a new share.
|
||||||
|
func (s *service) NewShare(ctx context.Context, shType EntityType, shID uuid.UUID, exp *time.Duration) (id string, err error) {
|
||||||
|
id, err = gonanoid.ID(SlugLength)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
store := FromCtx(ctx)
|
||||||
|
|
||||||
|
var expT *jsontypes.Time
|
||||||
|
if exp != nil {
|
||||||
|
tt := time.Now().Add(*exp)
|
||||||
|
expT = (*jsontypes.Time)(&tt)
|
||||||
|
}
|
||||||
|
|
||||||
|
share := &Share{
|
||||||
|
ID: id,
|
||||||
|
Type: shType,
|
||||||
|
EntityID: shID,
|
||||||
|
Expiration: expT,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = store.Create(ctx, share)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return id, nil
|
||||||
|
}
|
85
pkg/share/store.go
Normal file
85
pkg/share/store.go
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
package share
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"dynatron.me/x/stillbox/internal/jsontypes"
|
||||||
|
"dynatron.me/x/stillbox/pkg/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Store interface {
|
||||||
|
// Get retreives a share record.
|
||||||
|
Get(ctx context.Context, id string) (*Share, error)
|
||||||
|
|
||||||
|
// Create stores a new share record.
|
||||||
|
Create(ctx context.Context, share *Share) error
|
||||||
|
|
||||||
|
// Delete deletes a share record.
|
||||||
|
Delete(ctx context.Context, id string) error
|
||||||
|
|
||||||
|
// Prune removes expired share records.
|
||||||
|
Prune(ctx context.Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type postgresStore struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func recToShare(share database.Share) *Share {
|
||||||
|
return &Share{
|
||||||
|
ID: share.ID,
|
||||||
|
Type: EntityType(share.EntityType),
|
||||||
|
EntityID: share.EntityID,
|
||||||
|
Expiration: jsontypes.TimePtrFromTSTZ(share.Expiration),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *postgresStore) Get(ctx context.Context, id string) (*Share, error) {
|
||||||
|
db := database.FromCtx(ctx)
|
||||||
|
rec, err := db.GetShare(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return recToShare(rec), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *postgresStore) Create(ctx context.Context, share *Share) error {
|
||||||
|
db := database.FromCtx(ctx)
|
||||||
|
err := db.CreateShare(ctx, database.CreateShareParams{
|
||||||
|
ID: share.ID,
|
||||||
|
EntityType: string(share.Type),
|
||||||
|
EntityID: share.EntityID,
|
||||||
|
Expiration: share.Expiration.PGTypeTSTZ(),
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *postgresStore) Delete(ctx context.Context, id string) error {
|
||||||
|
return database.FromCtx(ctx).DeleteShare(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *postgresStore) Prune(ctx context.Context) error {
|
||||||
|
return database.FromCtx(ctx).PruneShares(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStore() *postgresStore {
|
||||||
|
return new(postgresStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
type storeCtxKey string
|
||||||
|
|
||||||
|
const StoreCtxKey storeCtxKey = "store"
|
||||||
|
|
||||||
|
func CtxWithStore(ctx context.Context, s Store) context.Context {
|
||||||
|
return context.WithValue(ctx, StoreCtxKey, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromCtx(ctx context.Context) Store {
|
||||||
|
s, ok := ctx.Value(StoreCtxKey).(Store)
|
||||||
|
if !ok {
|
||||||
|
return NewStore()
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
|
@ -2,15 +2,12 @@ package sinks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"dynatron.me/x/stillbox/internal/common"
|
|
||||||
"dynatron.me/x/stillbox/pkg/calls"
|
"dynatron.me/x/stillbox/pkg/calls"
|
||||||
|
"dynatron.me/x/stillbox/pkg/calls/callstore"
|
||||||
"dynatron.me/x/stillbox/pkg/database"
|
"dynatron.me/x/stillbox/pkg/database"
|
||||||
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
|
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -29,59 +26,9 @@ func (s *DatabaseSink) Call(ctx context.Context, call *calls.Call) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
params := s.toAddCallParams(call)
|
return callstore.FromCtx(ctx).AddCall(ctx, call)
|
||||||
|
|
||||||
err := s.db.InTx(ctx, func(tx database.Store) error {
|
|
||||||
err := tx.AddCall(ctx, params)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("add call: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug().Str("id", call.ID.String()).Int("system", call.System).Int("tgid", call.Talkgroup).Msg("stored")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}, pgx.TxOptions{})
|
|
||||||
|
|
||||||
if err != nil && database.IsTGConstraintViolation(err) {
|
|
||||||
return s.db.InTx(ctx, func(tx database.Store) error {
|
|
||||||
_, err := s.tgs.LearnTG(ctx, call)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("learn tg: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = tx.AddCall(ctx, params)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("learn tg retry: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}, pgx.TxOptions{})
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DatabaseSink) SinkType() string {
|
func (s *DatabaseSink) SinkType() string {
|
||||||
return "database"
|
return "database"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DatabaseSink) toAddCallParams(call *calls.Call) database.AddCallParams {
|
|
||||||
return database.AddCallParams{
|
|
||||||
ID: call.ID,
|
|
||||||
Submitter: call.Submitter.Int32Ptr(),
|
|
||||||
System: call.System,
|
|
||||||
Talkgroup: call.Talkgroup,
|
|
||||||
CallDate: pgtype.Timestamptz{Time: call.DateTime, Valid: true},
|
|
||||||
AudioName: common.NilIfZero(call.AudioName),
|
|
||||||
AudioBlob: call.Audio,
|
|
||||||
AudioType: common.NilIfZero(call.AudioType),
|
|
||||||
Duration: call.Duration.MsInt32Ptr(),
|
|
||||||
Frequency: call.Frequency,
|
|
||||||
Frequencies: call.Frequencies,
|
|
||||||
Patches: call.Patches,
|
|
||||||
TGLabel: call.TalkgroupLabel,
|
|
||||||
TGAlphaTag: call.TGAlphaTag,
|
|
||||||
TGGroup: call.TalkgroupGroup,
|
|
||||||
Source: call.Source,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -13,10 +13,10 @@ import (
|
||||||
|
|
||||||
"dynatron.me/x/stillbox/internal/common"
|
"dynatron.me/x/stillbox/internal/common"
|
||||||
"dynatron.me/x/stillbox/internal/forms"
|
"dynatron.me/x/stillbox/internal/forms"
|
||||||
"dynatron.me/x/stillbox/pkg/auth"
|
|
||||||
"dynatron.me/x/stillbox/pkg/calls"
|
"dynatron.me/x/stillbox/pkg/calls"
|
||||||
"dynatron.me/x/stillbox/pkg/config"
|
"dynatron.me/x/stillbox/pkg/config"
|
||||||
"dynatron.me/x/stillbox/pkg/sources"
|
"dynatron.me/x/stillbox/pkg/sources"
|
||||||
|
"dynatron.me/x/stillbox/pkg/users"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
@ -32,16 +32,16 @@ func TestRelay(t *testing.T) {
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
submitter auth.UserID
|
submitter users.UserID
|
||||||
apiKey string
|
apiKey string
|
||||||
call calls.Call
|
call calls.Call
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "base",
|
name: "base",
|
||||||
submitter: auth.UserID(1),
|
submitter: users.UserID(1),
|
||||||
call: calls.Call{
|
call: calls.Call{
|
||||||
ID: uuid.UUID([16]byte{0x52, 0xfd, 0xfc, 0x07, 0x21, 0x82, 0x45, 0x4f, 0x96, 0x3f, 0x5f, 0x0f, 0x9a, 0x62, 0x1d, 0x72}),
|
ID: uuid.UUID([16]byte{0x52, 0xfd, 0xfc, 0x07, 0x21, 0x82, 0x45, 0x4f, 0x96, 0x3f, 0x5f, 0x0f, 0x9a, 0x62, 0x1d, 0x72}),
|
||||||
Submitter: common.PtrTo(auth.UserID(1)),
|
Submitter: common.PtrTo(users.UserID(1)),
|
||||||
System: 197,
|
System: 197,
|
||||||
Talkgroup: 10101,
|
Talkgroup: 10101,
|
||||||
DateTime: time.Date(2024, 11, 10, 23, 33, 02, 0, time.Local),
|
DateTime: time.Date(2024, 11, 10, 23, 33, 02, 0, time.Local),
|
||||||
|
|
|
@ -9,6 +9,8 @@ import (
|
||||||
"dynatron.me/x/stillbox/internal/forms"
|
"dynatron.me/x/stillbox/internal/forms"
|
||||||
"dynatron.me/x/stillbox/pkg/auth"
|
"dynatron.me/x/stillbox/pkg/auth"
|
||||||
"dynatron.me/x/stillbox/pkg/calls"
|
"dynatron.me/x/stillbox/pkg/calls"
|
||||||
|
"dynatron.me/x/stillbox/pkg/rbac"
|
||||||
|
"dynatron.me/x/stillbox/pkg/users"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
@ -70,7 +72,7 @@ func (car *CallUploadRequest) mimeType() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (car *CallUploadRequest) ToCall(submitter auth.UserID) (*calls.Call, error) {
|
func (car *CallUploadRequest) ToCall(submitter users.UserID) (*calls.Call, error) {
|
||||||
return calls.Make(&calls.Call{
|
return calls.Make(&calls.Call{
|
||||||
Submitter: &submitter,
|
Submitter: &submitter,
|
||||||
System: car.System,
|
System: car.System,
|
||||||
|
@ -98,7 +100,13 @@ func (h *RdioHTTP) routeCallUpload(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
submitter, err := h.auth.CheckAPIKey(ctx, r.Form.Get("key"))
|
submitterSub, err := h.auth.CheckAPIKey(ctx, r.Form.Get("key"))
|
||||||
|
if err != nil {
|
||||||
|
auth.ErrorResponse(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
submitter, err := users.FromSubject(submitterSub)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
auth.ErrorResponse(w, err)
|
auth.ErrorResponse(w, err)
|
||||||
return
|
return
|
||||||
|
@ -117,20 +125,22 @@ func (h *RdioHTTP) routeCallUpload(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
call, err := cur.ToCall(*submitter)
|
call, err := cur.ToCall(submitter.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("toCall failed")
|
log.Error().Err(err).Msg("toCall failed")
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = h.ing.Ingest(ctx, call)
|
err = h.ing.Ingest(rbac.CtxWithSubject(ctx, submitterSub), call)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("ingest failed")
|
if rbac.ErrAccessDenied(err) != nil {
|
||||||
http.Error(w, "Call ingest failed.", http.StatusInternalServerError)
|
log.Error().Err(err).Msg("ingest failed")
|
||||||
|
http.Error(w, "Call ingest failed.", http.StatusForbidden)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info().Int("system", cur.System).Int("tgid", cur.Talkgroup).Str("duration", call.Duration.Duration().String()).Msg("ingested")
|
log.Info().Int("system", cur.System).Int("tgid", cur.Talkgroup).Str("duration", call.Duration.Duration().String()).Str("sub", submitter.Username).Msg("ingested")
|
||||||
|
|
||||||
written, err := w.Write([]byte("Call imported successfully."))
|
written, err := w.Write([]byte("Call imported successfully."))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -1,50 +0,0 @@
|
||||||
package store
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
|
|
||||||
"dynatron.me/x/stillbox/pkg/users"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Store interface {
|
|
||||||
TG() tgstore.Store
|
|
||||||
User() users.Store
|
|
||||||
}
|
|
||||||
|
|
||||||
type store struct {
|
|
||||||
tg tgstore.Store
|
|
||||||
user users.Store
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *store) TG() tgstore.Store {
|
|
||||||
return s.tg
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *store) User() users.Store {
|
|
||||||
return s.user
|
|
||||||
}
|
|
||||||
|
|
||||||
func New() Store {
|
|
||||||
return &store{
|
|
||||||
tg: tgstore.NewCache(),
|
|
||||||
user: users.NewStore(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type storeCtxKey string
|
|
||||||
|
|
||||||
const StoreCtxKey storeCtxKey = "store"
|
|
||||||
|
|
||||||
func CtxWithStore(ctx context.Context, s Store) context.Context {
|
|
||||||
return context.WithValue(ctx, StoreCtxKey, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func FromCtx(ctx context.Context) Store {
|
|
||||||
s, ok := ctx.Value(StoreCtxKey).(Store)
|
|
||||||
if !ok {
|
|
||||||
return New()
|
|
||||||
}
|
|
||||||
|
|
||||||
return s
|
|
||||||
}
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"dynatron.me/x/stillbox/pkg/database"
|
"dynatron.me/x/stillbox/pkg/database"
|
||||||
|
"dynatron.me/x/stillbox/pkg/rbac"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Talkgroup struct {
|
type Talkgroup struct {
|
||||||
|
@ -17,6 +18,10 @@ type Talkgroup struct {
|
||||||
Learned bool `json:"learned"`
|
Learned bool `json:"learned"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Talkgroup) GetResourceName() string {
|
||||||
|
return rbac.ResourceTalkgroup
|
||||||
|
}
|
||||||
|
|
||||||
func (t Talkgroup) String() string {
|
func (t Talkgroup) String() string {
|
||||||
if t.System.Name == "" {
|
if t.System.Name == "" {
|
||||||
t.System.Name = strconv.Itoa(int(t.Talkgroup.TGID))
|
t.System.Name = strconv.Itoa(int(t.Talkgroup.TGID))
|
||||||
|
|
|
@ -8,11 +8,12 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"dynatron.me/x/stillbox/internal/common"
|
"dynatron.me/x/stillbox/internal/common"
|
||||||
"dynatron.me/x/stillbox/pkg/auth"
|
|
||||||
"dynatron.me/x/stillbox/pkg/calls"
|
"dynatron.me/x/stillbox/pkg/calls"
|
||||||
"dynatron.me/x/stillbox/pkg/config"
|
"dynatron.me/x/stillbox/pkg/config"
|
||||||
"dynatron.me/x/stillbox/pkg/database"
|
"dynatron.me/x/stillbox/pkg/database"
|
||||||
|
"dynatron.me/x/stillbox/pkg/rbac"
|
||||||
tgsp "dynatron.me/x/stillbox/pkg/talkgroups"
|
tgsp "dynatron.me/x/stillbox/pkg/talkgroups"
|
||||||
|
"dynatron.me/x/stillbox/pkg/users"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
@ -176,7 +177,7 @@ func CtxWithStore(ctx context.Context, s Store) context.Context {
|
||||||
func FromCtx(ctx context.Context) Store {
|
func FromCtx(ctx context.Context) Store {
|
||||||
s, ok := ctx.Value(StoreCtxKey).(Store)
|
s, ok := ctx.Value(StoreCtxKey).(Store)
|
||||||
if !ok {
|
if !ok {
|
||||||
return NewCache()
|
panic("no tg store in context")
|
||||||
}
|
}
|
||||||
|
|
||||||
return s
|
return s
|
||||||
|
@ -201,19 +202,23 @@ type cache struct {
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
tgs tgMap
|
tgs tgMap
|
||||||
systems map[int]string
|
systems map[int]string
|
||||||
|
db database.Store
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCache returns a new cache Store.
|
// NewCache returns a new cache Store.
|
||||||
func NewCache() *cache {
|
func NewCache(db database.Store) *cache {
|
||||||
tgc := &cache{
|
tgc := &cache{
|
||||||
tgs: make(tgMap),
|
tgs: make(tgMap),
|
||||||
systems: make(map[int]string),
|
systems: make(map[int]string),
|
||||||
|
db: db,
|
||||||
}
|
}
|
||||||
|
|
||||||
return tgc
|
return tgc
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *cache) Hint(ctx context.Context, tgs []tgsp.ID) error {
|
func (t *cache) Hint(ctx context.Context, tgs []tgsp.ID) error {
|
||||||
|
// since this doesn't actually return data, we can skip rbac checks.
|
||||||
|
// This is only called by system services anyway.
|
||||||
if len(tgs) < 1 {
|
if len(tgs) < 1 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -322,11 +327,15 @@ func addToRowList[T rowType](t *cache, tgRecords []T) []*tgsp.Talkgroup {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *cache) TGs(ctx context.Context, tgs tgsp.IDs, opts ...Option) ([]*tgsp.Talkgroup, error) {
|
func (t *cache) TGs(ctx context.Context, tgs tgsp.IDs, opts ...Option) ([]*tgsp.Talkgroup, error) {
|
||||||
db := database.FromCtx(ctx)
|
_, err := rbac.Check(ctx, rbac.UseResource(rbac.ResourceTalkgroup), rbac.WithActions(rbac.ActionRead))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
db := t.db
|
||||||
|
|
||||||
r := make([]*tgsp.Talkgroup, 0, len(tgs))
|
r := make([]*tgsp.Talkgroup, 0, len(tgs))
|
||||||
opt := sOpt(opts)
|
opt := sOpt(opts)
|
||||||
var err error
|
|
||||||
if tgs != nil {
|
if tgs != nil {
|
||||||
toGet := make(tgsp.IDs, 0, len(tgs))
|
toGet := make(tgsp.IDs, 0, len(tgs))
|
||||||
for _, id := range tgs {
|
for _, id := range tgs {
|
||||||
|
@ -394,7 +403,8 @@ func (t *cache) TGs(ctx context.Context, tgs tgsp.IDs, opts ...Option) ([]*tgsp.
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *cache) Load(ctx context.Context, tgs database.TGTuples) error {
|
func (t *cache) Load(ctx context.Context, tgs database.TGTuples) error {
|
||||||
tgRecords, err := database.FromCtx(ctx).GetTalkgroupsWithLearnedBySysTGID(ctx, tgs)
|
// No need for RBAC checks since this merely primes the cache and returns nothing.
|
||||||
|
tgRecords, err := t.db.GetTalkgroupsWithLearnedBySysTGID(ctx, tgs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -420,9 +430,13 @@ func (t *cache) Weight(ctx context.Context, id tgsp.ID, tm time.Time) float64 {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *cache) SystemTGs(ctx context.Context, systemID int, opts ...Option) ([]*tgsp.Talkgroup, error) {
|
func (t *cache) SystemTGs(ctx context.Context, systemID int, opts ...Option) ([]*tgsp.Talkgroup, error) {
|
||||||
db := database.FromCtx(ctx)
|
_, err := rbac.Check(ctx, rbac.UseResource(rbac.ResourceTalkgroup), rbac.WithActions(rbac.ActionRead))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
db := t.db
|
||||||
opt := sOpt(opts)
|
opt := sOpt(opts)
|
||||||
var err error
|
|
||||||
if opt.pagination != nil {
|
if opt.pagination != nil {
|
||||||
sortDir, err := opt.pagination.SortDir()
|
sortDir, err := opt.pagination.SortDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -472,13 +486,18 @@ func (t *cache) SystemTGs(ctx context.Context, systemID int, opts ...Option) ([]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *cache) TG(ctx context.Context, tg tgsp.ID) (*tgsp.Talkgroup, error) {
|
func (t *cache) TG(ctx context.Context, tg tgsp.ID) (*tgsp.Talkgroup, error) {
|
||||||
|
_, err := rbac.Check(ctx, rbac.UseResource(rbac.ResourceTalkgroup), rbac.WithActions(rbac.ActionRead))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
rec, has := t.get(tg)
|
rec, has := t.get(tg)
|
||||||
|
|
||||||
if has {
|
if has {
|
||||||
return rec, nil
|
return rec, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
record, err := database.FromCtx(ctx).GetTalkgroupWithLearned(ctx, int32(tg.System), int32(tg.Talkgroup))
|
record, err := t.db.GetTalkgroupWithLearned(ctx, int32(tg.System), int32(tg.Talkgroup))
|
||||||
switch err {
|
switch err {
|
||||||
case nil:
|
case nil:
|
||||||
case pgx.ErrNoRows:
|
case pgx.ErrNoRows:
|
||||||
|
@ -494,12 +513,17 @@ func (t *cache) TG(ctx context.Context, tg tgsp.ID) (*tgsp.Talkgroup, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *cache) SystemName(ctx context.Context, id int) (name string, has bool) {
|
func (t *cache) SystemName(ctx context.Context, id int) (name string, has bool) {
|
||||||
|
_, err := rbac.Check(ctx, rbac.UseResource(rbac.ResourceTalkgroup), rbac.WithActions(rbac.ActionRead))
|
||||||
|
if err != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
t.RLock()
|
t.RLock()
|
||||||
n, has := t.systems[id]
|
n, has := t.systems[id]
|
||||||
t.RUnlock()
|
t.RUnlock()
|
||||||
|
|
||||||
if !has {
|
if !has {
|
||||||
sys, err := database.FromCtx(ctx).GetSystemName(ctx, id)
|
sys, err := t.db.GetSystemName(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
@ -515,20 +539,26 @@ func (t *cache) SystemName(ctx context.Context, id int) (name string, has bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *cache) UpdateTG(ctx context.Context, input database.UpdateTalkgroupParams) (*tgsp.Talkgroup, error) {
|
func (t *cache) UpdateTG(ctx context.Context, input database.UpdateTalkgroupParams) (*tgsp.Talkgroup, error) {
|
||||||
|
user, err := users.UserCheck(ctx, new(tgsp.Talkgroup), "update")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
sysName, has := t.SystemName(ctx, int(*input.SystemID))
|
sysName, has := t.SystemName(ctx, int(*input.SystemID))
|
||||||
if !has {
|
if !has {
|
||||||
return nil, ErrNoSuchSystem
|
return nil, ErrNoSuchSystem
|
||||||
}
|
}
|
||||||
db := database.FromCtx(ctx)
|
|
||||||
|
db := t.db
|
||||||
var tg database.Talkgroup
|
var tg database.Talkgroup
|
||||||
err := db.InTx(ctx, func(db database.Store) error {
|
err = db.InTx(ctx, func(db database.Store) error {
|
||||||
var oerr error
|
var oerr error
|
||||||
tg, oerr = db.UpdateTalkgroup(ctx, input)
|
tg, oerr = db.UpdateTalkgroup(ctx, input)
|
||||||
if oerr != nil {
|
if oerr != nil {
|
||||||
return oerr
|
return oerr
|
||||||
}
|
}
|
||||||
versionBatch := db.StoreTGVersion(ctx, []database.StoreTGVersionParams{{
|
versionBatch := db.StoreTGVersion(ctx, []database.StoreTGVersionParams{{
|
||||||
Submitter: auth.UIDFrom(ctx),
|
Submitter: user.ID.Int32Ptr(),
|
||||||
TGID: *input.TGID,
|
TGID: *input.TGID,
|
||||||
}})
|
}})
|
||||||
defer versionBatch.Close()
|
defer versionBatch.Close()
|
||||||
|
@ -557,12 +587,17 @@ func (t *cache) UpdateTG(ctx context.Context, input database.UpdateTalkgroupPara
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *cache) DeleteSystem(ctx context.Context, id int) error {
|
func (t *cache) DeleteSystem(ctx context.Context, id int) error {
|
||||||
|
_, err := rbac.Check(ctx, rbac.UseResource(rbac.ResourceTalkgroup), rbac.WithActions(rbac.ActionDelete))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
t.Lock()
|
t.Lock()
|
||||||
defer t.Unlock()
|
defer t.Unlock()
|
||||||
|
|
||||||
t.invalidate()
|
t.invalidate()
|
||||||
|
|
||||||
err := database.FromCtx(ctx).DeleteSystem(ctx, id)
|
err = t.db.DeleteSystem(ctx, id)
|
||||||
switch {
|
switch {
|
||||||
case err == nil:
|
case err == nil:
|
||||||
return nil
|
return nil
|
||||||
|
@ -574,11 +609,21 @@ func (t *cache) DeleteSystem(ctx context.Context, id int) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *cache) DeleteTG(ctx context.Context, id tgsp.ID) error {
|
func (t *cache) DeleteTG(ctx context.Context, id tgsp.ID) error {
|
||||||
|
_, err := rbac.Check(ctx, rbac.UseResource(rbac.ResourceTalkgroup), rbac.WithActions(rbac.ActionDelete))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
t.Lock()
|
t.Lock()
|
||||||
defer t.Unlock()
|
defer t.Unlock()
|
||||||
|
|
||||||
err := database.FromCtx(ctx).InTx(ctx, func(db database.Store) error {
|
user, err := users.UserCheck(ctx, new(tgsp.Talkgroup), "update")
|
||||||
err := db.StoreDeletedTGVersion(ctx, common.PtrTo(int32(id.System)), common.PtrTo(int32(id.Talkgroup)), auth.UIDFrom(ctx))
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = t.db.InTx(ctx, func(db database.Store) error {
|
||||||
|
err := db.StoreDeletedTGVersion(ctx, common.PtrTo(int32(id.System)), common.PtrTo(int32(id.Talkgroup)), user.ID.Int32Ptr())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -600,7 +645,12 @@ func (t *cache) DeleteTG(ctx context.Context, id tgsp.ID) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *cache) LearnTG(ctx context.Context, c *calls.Call) (*tgsp.Talkgroup, error) {
|
func (t *cache) LearnTG(ctx context.Context, c *calls.Call) (*tgsp.Talkgroup, error) {
|
||||||
db := database.FromCtx(ctx)
|
_, err := rbac.Check(ctx, rbac.UseResource(rbac.ResourceTalkgroup), rbac.WithActions(rbac.ActionCreate, rbac.ActionUpdate))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
db := t.db
|
||||||
|
|
||||||
sys, has := t.SystemName(ctx, c.System)
|
sys, has := t.SystemName(ctx, c.System)
|
||||||
if !has {
|
if !has {
|
||||||
|
@ -633,7 +683,12 @@ func (t *cache) LearnTG(ctx context.Context, c *calls.Call) (*tgsp.Talkgroup, er
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *cache) UpsertTGs(ctx context.Context, system int, input []database.UpsertTalkgroupParams) ([]*tgsp.Talkgroup, error) {
|
func (t *cache) UpsertTGs(ctx context.Context, system int, input []database.UpsertTalkgroupParams) ([]*tgsp.Talkgroup, error) {
|
||||||
db := database.FromCtx(ctx)
|
user, err := users.UserCheck(ctx, new(tgsp.Talkgroup), "create+update")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
db := t.db
|
||||||
sysName, hasSys := t.SystemName(ctx, system)
|
sysName, hasSys := t.SystemName(ctx, system)
|
||||||
if !hasSys {
|
if !hasSys {
|
||||||
return nil, ErrNoSuchSystem
|
return nil, ErrNoSuchSystem
|
||||||
|
@ -645,7 +700,7 @@ func (t *cache) UpsertTGs(ctx context.Context, system int, input []database.Upse
|
||||||
|
|
||||||
tgs := make([]*tgsp.Talkgroup, 0, len(input))
|
tgs := make([]*tgsp.Talkgroup, 0, len(input))
|
||||||
|
|
||||||
err := db.InTx(ctx, func(db database.Store) error {
|
err = db.InTx(ctx, func(db database.Store) error {
|
||||||
versionParams := make([]database.StoreTGVersionParams, 0, len(input))
|
versionParams := make([]database.StoreTGVersionParams, 0, len(input))
|
||||||
for i := range input {
|
for i := range input {
|
||||||
// normalize tags
|
// normalize tags
|
||||||
|
@ -670,7 +725,7 @@ func (t *cache) UpsertTGs(ctx context.Context, system int, input []database.Upse
|
||||||
versionParams = append(versionParams, database.StoreTGVersionParams{
|
versionParams = append(versionParams, database.StoreTGVersionParams{
|
||||||
SystemID: int32(system),
|
SystemID: int32(system),
|
||||||
TGID: r.TGID,
|
TGID: r.TGID,
|
||||||
Submitter: auth.UIDFrom(ctx),
|
Submitter: user.ID.Int32Ptr(),
|
||||||
})
|
})
|
||||||
tgs = append(tgs, &tgsp.Talkgroup{
|
tgs = append(tgs, &tgsp.Talkgroup{
|
||||||
Talkgroup: r,
|
Talkgroup: r,
|
||||||
|
@ -709,14 +764,24 @@ func (t *cache) UpsertTGs(ctx context.Context, system int, input []database.Upse
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *cache) CreateSystem(ctx context.Context, id int, name string) error {
|
func (t *cache) CreateSystem(ctx context.Context, id int, name string) error {
|
||||||
|
_, err := rbac.Check(ctx, rbac.UseResource(rbac.ResourceTalkgroup), rbac.WithActions(rbac.ActionCreate))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
t.Lock()
|
t.Lock()
|
||||||
defer t.Unlock()
|
defer t.Unlock()
|
||||||
|
|
||||||
t.addSysNoLock(id, name)
|
t.addSysNoLock(id, name)
|
||||||
|
|
||||||
return database.FromCtx(ctx).CreateSystem(ctx, id, name)
|
return t.db.CreateSystem(ctx, id, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *cache) Tags(ctx context.Context) ([]string, error) {
|
func (t *cache) Tags(ctx context.Context) ([]string, error) {
|
||||||
return database.FromCtx(ctx).GetAllTalkgroupTags(ctx)
|
_, err := rbac.Check(ctx, rbac.UseResource(rbac.ResourceTalkgroup), rbac.WithActions(rbac.ActionRead))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.db.GetAllTalkgroupTags(ctx)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,9 +14,12 @@ import (
|
||||||
|
|
||||||
"dynatron.me/x/stillbox/pkg/database"
|
"dynatron.me/x/stillbox/pkg/database"
|
||||||
"dynatron.me/x/stillbox/pkg/database/mocks"
|
"dynatron.me/x/stillbox/pkg/database/mocks"
|
||||||
|
"dynatron.me/x/stillbox/pkg/rbac"
|
||||||
|
rbacmocks "dynatron.me/x/stillbox/pkg/rbac/mocks"
|
||||||
"dynatron.me/x/stillbox/pkg/talkgroups"
|
"dynatron.me/x/stillbox/pkg/talkgroups"
|
||||||
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
|
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
|
||||||
"dynatron.me/x/stillbox/pkg/talkgroups/xport"
|
"dynatron.me/x/stillbox/pkg/talkgroups/xport"
|
||||||
|
"dynatron.me/x/stillbox/pkg/users"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getFixture(fixture string) []byte {
|
func getFixture(fixture string) []byte {
|
||||||
|
@ -51,14 +54,19 @@ func TestRadioRef(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
subject := users.User{IsAdmin: true}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
dbMock := mocks.NewStore(t)
|
dbMock := mocks.NewStore(t)
|
||||||
|
rbacMock := rbacmocks.NewRBAC(t)
|
||||||
|
rbacMock.EXPECT().Check(mock.AnythingOfType("*context.valueCtx"), rbac.UseResource("Talkgroup"), mock.AnythingOfType("rbac.CheckOption")).Return(&subject, nil)
|
||||||
if tc.expectErr == nil {
|
if tc.expectErr == nil {
|
||||||
dbMock.EXPECT().GetSystemName(mock.AnythingOfType("*context.valueCtx"), tc.sysID).Return(tc.sysName, nil)
|
dbMock.EXPECT().GetSystemName(mock.AnythingOfType("*context.valueCtx"), tc.sysID).Return(tc.sysName, nil)
|
||||||
}
|
}
|
||||||
ctx := database.CtxWithDB(context.Background(), dbMock)
|
ctx := database.CtxWithDB(context.Background(), dbMock)
|
||||||
ctx = tgstore.CtxWithStore(ctx, tgstore.NewCache())
|
ctx = rbac.CtxWithRBAC(ctx, rbacMock)
|
||||||
|
ctx = tgstore.CtxWithStore(ctx, tgstore.NewCache(dbMock))
|
||||||
ij := &xport.ImportJob{
|
ij := &xport.ImportJob{
|
||||||
Type: xport.Format(tc.impType),
|
Type: xport.Format(tc.impType),
|
||||||
SystemID: tc.sysID,
|
SystemID: tc.sysID,
|
||||||
|
|
21
pkg/users/guest.go
Normal file
21
pkg/users/guest.go
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package users
|
||||||
|
|
||||||
|
import (
|
||||||
|
"dynatron.me/x/stillbox/pkg/rbac"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ShareLinkGuest struct {
|
||||||
|
ShareID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ShareLinkGuest) GetRoles() []string {
|
||||||
|
return []string{rbac.RoleShareGuest}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Public struct {
|
||||||
|
RemoteAddr string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Public) GetRoles() []string {
|
||||||
|
return []string{rbac.RolePublic}
|
||||||
|
}
|
|
@ -3,22 +3,40 @@ package users
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"dynatron.me/x/stillbox/internal/cache"
|
||||||
"dynatron.me/x/stillbox/pkg/database"
|
"dynatron.me/x/stillbox/pkg/database"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Store interface {
|
type Store interface {
|
||||||
|
// GetUser gets a user by UID.
|
||||||
|
GetUser(ctx context.Context, username string) (*User, error)
|
||||||
|
|
||||||
// UserPrefs gets the preferences for the specified user and app name.
|
// UserPrefs gets the preferences for the specified user and app name.
|
||||||
UserPrefs(ctx context.Context, uid int32, appName string) ([]byte, error)
|
UserPrefs(ctx context.Context, username string, appName string) ([]byte, error)
|
||||||
|
|
||||||
// SetUserPrefs sets the preferences for the specified user and app name.
|
// SetUserPrefs sets the preferences for the specified user and app name.
|
||||||
SetUserPrefs(ctx context.Context, uid int32, appName string, prefs []byte) error
|
SetUserPrefs(ctx context.Context, username string, appName string, prefs []byte) error
|
||||||
|
|
||||||
|
// Invalidate clears the user cache.
|
||||||
|
Invalidate()
|
||||||
|
|
||||||
|
// UpdateUser updates a user's record
|
||||||
|
UpdateUser(ctx context.Context, username string, user UserUpdate) error
|
||||||
|
|
||||||
|
// GetUserByAPIKey gets a user by API key.
|
||||||
|
GetAPIKey(ctx context.Context, key string) (database.GetAPIKeyRow, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type store struct {
|
type postgresStore struct {
|
||||||
|
cache.Cache[string, *User]
|
||||||
|
db database.Store
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStore() *store {
|
func NewStore(db database.Store) *postgresStore {
|
||||||
return new(store)
|
return &postgresStore{
|
||||||
|
Cache: cache.New[string, *User](),
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type storeCtxKey string
|
type storeCtxKey string
|
||||||
|
@ -32,16 +50,56 @@ func CtxWithStore(ctx context.Context, s Store) context.Context {
|
||||||
func FromCtx(ctx context.Context) Store {
|
func FromCtx(ctx context.Context) Store {
|
||||||
s, ok := ctx.Value(StoreCtxKey).(Store)
|
s, ok := ctx.Value(StoreCtxKey).(Store)
|
||||||
if !ok {
|
if !ok {
|
||||||
return NewStore()
|
panic("no users store in context")
|
||||||
}
|
}
|
||||||
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *store) UserPrefs(ctx context.Context, uid int32, appName string) ([]byte, error) {
|
func (s *postgresStore) Invalidate() {
|
||||||
db := database.FromCtx(ctx)
|
s.Clear()
|
||||||
|
}
|
||||||
|
|
||||||
prefs, err := db.GetAppPrefs(ctx, appName, int(uid))
|
type UserUpdate struct {
|
||||||
|
Email *string `json:"email"`
|
||||||
|
IsAdmin *bool `json:"isAdmin"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *postgresStore) UpdateUser(ctx context.Context, username string, user UserUpdate) error {
|
||||||
|
dbu, err := s.db.UpdateUser(ctx, username, user.Email, user.IsAdmin)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Set(username, fromDBUser(dbu))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *postgresStore) GetUser(ctx context.Context, username string) (*User, error) {
|
||||||
|
u, has := s.Get(username)
|
||||||
|
if has {
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dbu, err := s.db.GetUserByUsername(ctx, username)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
u = fromDBUser(dbu)
|
||||||
|
s.Set(username, u)
|
||||||
|
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *postgresStore) UserPrefs(ctx context.Context, username string, appName string) ([]byte, error) {
|
||||||
|
u, err := s.GetUser(ctx, username)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
prefs, err := s.db.GetAppPrefs(ctx, appName, int(u.ID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -49,8 +107,15 @@ func (s *store) UserPrefs(ctx context.Context, uid int32, appName string) ([]byt
|
||||||
return []byte(prefs), err
|
return []byte(prefs), err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *store) SetUserPrefs(ctx context.Context, uid int32, appName string, prefs []byte) error {
|
func (s *postgresStore) SetUserPrefs(ctx context.Context, username string, appName string, prefs []byte) error {
|
||||||
db := database.FromCtx(ctx)
|
u, err := s.GetUser(ctx, username)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return db.SetAppPrefs(ctx, appName, prefs, int(uid))
|
return s.db.SetAppPrefs(ctx, appName, prefs, int(u.ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *postgresStore) GetAPIKey(ctx context.Context, b64hash string) (database.GetAPIKeyRow, error) {
|
||||||
|
return s.db.GetAPIKey(ctx, b64hash)
|
||||||
}
|
}
|
||||||
|
|
94
pkg/users/user.go
Normal file
94
pkg/users/user.go
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
package users
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"dynatron.me/x/stillbox/pkg/database"
|
||||||
|
"dynatron.me/x/stillbox/pkg/rbac"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserID int
|
||||||
|
|
||||||
|
func (u *UserID) Int32Ptr() *int32 {
|
||||||
|
if u == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
i := int32(*u)
|
||||||
|
|
||||||
|
return &i
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u UserID) Int() int {
|
||||||
|
return int(u)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u UserID) IsValid() bool {
|
||||||
|
return u > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func From(ctx context.Context) (*User, error) {
|
||||||
|
sub := rbac.SubjectFrom(ctx)
|
||||||
|
return FromSubject(sub)
|
||||||
|
}
|
||||||
|
|
||||||
|
func UserCheck(ctx context.Context, rsc rbac.Resource, actions string) (*User, error) {
|
||||||
|
acts := strings.Split(actions, "+")
|
||||||
|
subj, err := rbac.FromCtx(ctx).Check(ctx, rsc, rbac.WithActions(acts...))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return FromSubject(subj)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromSubject(sub rbac.Subject) (*User, error) {
|
||||||
|
if sub == nil {
|
||||||
|
return nil, rbac.ErrBadSubject
|
||||||
|
}
|
||||||
|
|
||||||
|
user, isUser := sub.(*User)
|
||||||
|
if !isUser || user == nil || !user.ID.IsValid() {
|
||||||
|
return nil, rbac.ErrBadSubject
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
ID UserID
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
Email string
|
||||||
|
IsAdmin bool
|
||||||
|
Prefs json.RawMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) GetName() string {
|
||||||
|
return u.Username
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) GetRoles() []string {
|
||||||
|
r := make([]string, 1, 2)
|
||||||
|
|
||||||
|
r[0] = rbac.RoleUser
|
||||||
|
|
||||||
|
if u.IsAdmin {
|
||||||
|
r = append(r, rbac.RoleAdmin)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromDBUser(dbu database.User) *User {
|
||||||
|
return &User{
|
||||||
|
ID: UserID(dbu.ID),
|
||||||
|
Username: dbu.Username,
|
||||||
|
Password: dbu.Password,
|
||||||
|
Email: dbu.Email,
|
||||||
|
IsAdmin: dbu.IsAdmin,
|
||||||
|
Prefs: dbu.Prefs,
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
CREATE TABLE IF NOT EXISTS users(
|
CREATE TABLE IF NOT EXISTS users(
|
||||||
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY (START WITH 1),
|
||||||
username VARCHAR (255) UNIQUE NOT NULL,
|
username VARCHAR (255) UNIQUE NOT NULL,
|
||||||
password TEXT NOT NULL,
|
password TEXT NOT NULL,
|
||||||
email TEXT NOT NULL,
|
email TEXT NOT NULL,
|
||||||
|
@ -141,6 +141,7 @@ CREATE TABLE IF NOT EXISTS settings(
|
||||||
CREATE TABLE IF NOT EXISTS incidents(
|
CREATE TABLE IF NOT EXISTS incidents(
|
||||||
id UUID PRIMARY KEY,
|
id UUID PRIMARY KEY,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
|
owner INTEGER NOT NULL,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
start_time TIMESTAMPTZ,
|
start_time TIMESTAMPTZ,
|
||||||
end_time TIMESTAMPTZ,
|
end_time TIMESTAMPTZ,
|
||||||
|
@ -163,3 +164,11 @@ CREATE TABLE IF NOT EXISTS incidents_calls(
|
||||||
FOREIGN KEY (calls_tbl_id, call_date) REFERENCES calls(id, call_date),
|
FOREIGN KEY (calls_tbl_id, call_date) REFERENCES calls(id, call_date),
|
||||||
PRIMARY KEY (incident_id, call_id)
|
PRIMARY KEY (incident_id, call_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS shares(
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
entity_type TEXT NOT NULL,
|
||||||
|
entity_id UUID NOT NULL,
|
||||||
|
owner INTEGER NOT NULL REFERENCES users(id),
|
||||||
|
expiration TIMESTAMPTZ NULL
|
||||||
|
);
|
||||||
|
|
|
@ -156,3 +156,9 @@ CASE WHEN sqlc.narg('tags_not')::TEXT[] IS NOT NULL THEN
|
||||||
c.duration > @longer_than
|
c.duration > @longer_than
|
||||||
) ELSE TRUE END)
|
) ELSE TRUE END)
|
||||||
;
|
;
|
||||||
|
|
||||||
|
-- name: DeleteCall :exec
|
||||||
|
DELETE FROM calls WHERE id = @id;
|
||||||
|
|
||||||
|
-- name: GetCallSubmitter :one
|
||||||
|
SELECT submitter FROM calls WHERE id = @id;
|
||||||
|
|
|
@ -33,6 +33,7 @@ WHERE incident_id = @incident_id AND call_id = @call_id;
|
||||||
INSERT INTO incidents (
|
INSERT INTO incidents (
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
|
owner,
|
||||||
description,
|
description,
|
||||||
start_time,
|
start_time,
|
||||||
end_time,
|
end_time,
|
||||||
|
@ -41,6 +42,7 @@ INSERT INTO incidents (
|
||||||
) VALUES (
|
) VALUES (
|
||||||
@id,
|
@id,
|
||||||
@name,
|
@name,
|
||||||
|
@owner,
|
||||||
sqlc.narg('description'),
|
sqlc.narg('description'),
|
||||||
sqlc.narg('start_time'),
|
sqlc.narg('start_time'),
|
||||||
sqlc.narg('end_time'),
|
sqlc.narg('end_time'),
|
||||||
|
@ -54,6 +56,7 @@ RETURNING *;
|
||||||
SELECT
|
SELECT
|
||||||
i.id,
|
i.id,
|
||||||
i.name,
|
i.name,
|
||||||
|
i.owner,
|
||||||
i.description,
|
i.description,
|
||||||
i.start_time,
|
i.start_time,
|
||||||
i.end_time,
|
i.end_time,
|
||||||
|
@ -148,6 +151,7 @@ ORDER BY ic.call_date ASC;
|
||||||
SELECT
|
SELECT
|
||||||
i.id,
|
i.id,
|
||||||
i.name,
|
i.name,
|
||||||
|
i.owner,
|
||||||
i.description,
|
i.description,
|
||||||
i.start_time,
|
i.start_time,
|
||||||
i.end_time,
|
i.end_time,
|
||||||
|
@ -171,3 +175,6 @@ RETURNING *;
|
||||||
|
|
||||||
-- name: DeleteIncident :exec
|
-- name: DeleteIncident :exec
|
||||||
DELETE FROM incidents CASCADE WHERE id = @id;
|
DELETE FROM incidents CASCADE WHERE id = @id;
|
||||||
|
|
||||||
|
-- name: GetIncidentOwner :one
|
||||||
|
SELECT owner FROM incidents WHERE id = @id;
|
||||||
|
|
24
sql/postgres/queries/share.sql
Normal file
24
sql/postgres/queries/share.sql
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
-- name: GetShare :one
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
entity_type,
|
||||||
|
entity_id,
|
||||||
|
owner,
|
||||||
|
expiration
|
||||||
|
FROM shares
|
||||||
|
WHERE id = @id;
|
||||||
|
|
||||||
|
-- name: CreateShare :exec
|
||||||
|
INSERT INTO shares (
|
||||||
|
id,
|
||||||
|
entity_type,
|
||||||
|
entity_id,
|
||||||
|
owner,
|
||||||
|
expiration
|
||||||
|
) VALUES (@id, @entity_type, @entity_id, @owner, sqlc.narg('expiration'));
|
||||||
|
|
||||||
|
-- name: DeleteShare :exec
|
||||||
|
DELETE FROM shares WHERE id = @id;
|
||||||
|
|
||||||
|
-- name: PruneShares :exec
|
||||||
|
DELETE FROM shares WHERE expiration < NOW();
|
|
@ -1,14 +1,10 @@
|
||||||
-- name: GetUserByID :one
|
-- name: GetUserByID :one
|
||||||
SELECT * FROM users
|
SELECT * FROM users
|
||||||
WHERE id = $1 LIMIT 1;
|
WHERE id = $1;
|
||||||
|
|
||||||
-- name: GetUserByUsername :one
|
-- name: GetUserByUsername :one
|
||||||
SELECT * FROM users
|
SELECT * FROM users
|
||||||
WHERE username = $1 LIMIT 1;
|
WHERE username = $1;
|
||||||
|
|
||||||
-- name: GetUserByUID :one
|
|
||||||
SELECT * FROM users
|
|
||||||
WHERE id = $1 LIMIT 1;
|
|
||||||
|
|
||||||
-- name: GetUsers :many
|
-- name: GetUsers :many
|
||||||
SELECT * FROM users;
|
SELECT * FROM users;
|
||||||
|
@ -28,6 +24,14 @@ DELETE FROM users WHERE username = $1;
|
||||||
-- name: UpdatePassword :exec
|
-- name: UpdatePassword :exec
|
||||||
UPDATE users SET password = $2 WHERE username = $1;
|
UPDATE users SET password = $2 WHERE username = $1;
|
||||||
|
|
||||||
|
-- name: UpdateUser :one
|
||||||
|
UPDATE users SET
|
||||||
|
email = COALESCE(sqlc.narg('email'), email),
|
||||||
|
is_admin = COALESCE(sqlc.narg('is_admin'), is_admin)
|
||||||
|
WHERE
|
||||||
|
username = $1
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
-- name: CreateAPIKey :one
|
-- name: CreateAPIKey :one
|
||||||
INSERT INTO api_keys(
|
INSERT INTO api_keys(
|
||||||
owner,
|
owner,
|
||||||
|
@ -42,7 +46,17 @@ RETURNING *;
|
||||||
DELETE FROM api_keys WHERE api_key = $1;
|
DELETE FROM api_keys WHERE api_key = $1;
|
||||||
|
|
||||||
-- name: GetAPIKey :one
|
-- name: GetAPIKey :one
|
||||||
SELECT * FROM api_keys WHERE api_key = $1;
|
SELECT
|
||||||
|
a.id,
|
||||||
|
a.owner,
|
||||||
|
a.created_at,
|
||||||
|
a.expires,
|
||||||
|
a.disabled,
|
||||||
|
a.api_key,
|
||||||
|
u.username
|
||||||
|
FROM api_keys a
|
||||||
|
JOIN users u ON (a.owner = u.id)
|
||||||
|
WHERE api_key = $1;
|
||||||
|
|
||||||
-- name: GetAppPrefs :one
|
-- name: GetAppPrefs :one
|
||||||
SELECT (prefs->>(@app_name::TEXT))::JSONB FROM users WHERE id = @uid;
|
SELECT (prefs->>(@app_name::TEXT))::JSONB FROM users WHERE id = @uid;
|
||||||
|
|
Loading…
Reference in a new issue