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
.mockery.yamlgo.modgo.sum
internal
pkg
alerting
auth
calls
database
incidents
nexus
rbac
rest
server
share
sinks
sources
store
talkgroups
users
sql/postgres
|
@ -1,4 +1,4 @@
|
|||
dir: '{{ replaceAll .InterfaceDirRelative "internal" "internal_" }}/mocks'
|
||||
dir: '{{.InterfaceDir}}/mocks'
|
||||
mockname: "{{.InterfaceName}}"
|
||||
outpkg: "mocks"
|
||||
filename: "{{.InterfaceName}}.go"
|
||||
|
@ -9,3 +9,7 @@ packages:
|
|||
interfaces:
|
||||
Store:
|
||||
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/davecgh/go-spew v1.1.1 // 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/go-audio/audio 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/jwx/v2 v2.1.3 // 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-isatty v0.0.20 // 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/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/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/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
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-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
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/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
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/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/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/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
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/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
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/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
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/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
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/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
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.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.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/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
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.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
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/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
||||
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-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/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/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-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
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.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
||||
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/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
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/forms"
|
||||
"dynatron.me/x/stillbox/pkg/auth"
|
||||
"dynatron.me/x/stillbox/pkg/calls"
|
||||
"dynatron.me/x/stillbox/pkg/sources"
|
||||
"dynatron.me/x/stillbox/pkg/users"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
@ -62,16 +62,16 @@ func TestMarshal(t *testing.T) {
|
|||
|
||||
tests := []struct {
|
||||
name string
|
||||
submitter auth.UserID
|
||||
submitter users.UserID
|
||||
apiKey string
|
||||
call calls.Call
|
||||
}{
|
||||
{
|
||||
name: "base",
|
||||
submitter: auth.UserID(1),
|
||||
submitter: users.UserID(1),
|
||||
call: calls.Call{
|
||||
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,
|
||||
Talkgroup: 10101,
|
||||
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/database"
|
||||
"dynatron.me/x/stillbox/pkg/notify"
|
||||
"dynatron.me/x/stillbox/pkg/rbac"
|
||||
"dynatron.me/x/stillbox/pkg/sinks"
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups"
|
||||
"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.
|
||||
func (as *alerter) Go(ctx context.Context) {
|
||||
ctx = rbac.CtxWithSubject(ctx, &rbac.SystemServiceSubject{Name: "alerter"})
|
||||
|
||||
err := as.startBackfill(ctx)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("backfill")
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"dynatron.me/x/stillbox/internal/jsontypes"
|
||||
"dynatron.me/x/stillbox/internal/trending"
|
||||
"dynatron.me/x/stillbox/pkg/config"
|
||||
"dynatron.me/x/stillbox/pkg/database"
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups"
|
||||
"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.
|
||||
func (s *Simulation) Simulate(ctx context.Context) (trending.Scores[talkgroups.ID], error) {
|
||||
db := database.FromCtx(ctx)
|
||||
now := time.Now()
|
||||
tgc := tgstore.NewCache()
|
||||
tgc := tgstore.NewCache(db)
|
||||
|
||||
s.Enable = true
|
||||
s.alerter = New(s.Alerting, tgc, WithClock(&s.clock)).(*alerter)
|
||||
|
|
|
@ -7,28 +7,28 @@ import (
|
|||
"time"
|
||||
|
||||
"dynatron.me/x/stillbox/pkg/database"
|
||||
"dynatron.me/x/stillbox/pkg/rbac"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
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.
|
||||
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)
|
||||
if err != nil {
|
||||
log.Error().Str("apikey", key).Msg("cannot parse key")
|
||||
return nil, ErrBadRequest
|
||||
}
|
||||
|
||||
db := database.FromCtx(ctx)
|
||||
hash := sha256.Sum256([]byte(keyUuid.String()))
|
||||
b64hash := base64.StdEncoding.EncodeToString(hash[:])
|
||||
apik, err := db.GetAPIKey(ctx, b64hash)
|
||||
apik, err := a.ust.GetAPIKey(ctx, b64hash)
|
||||
if err != nil {
|
||||
if database.IsNoRows(err) {
|
||||
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
|
||||
}
|
||||
|
||||
owner := UserID(apik.Owner)
|
||||
|
||||
return &owner, nil
|
||||
return a.ust.GetUser(ctx, apik.Username)
|
||||
}
|
||||
|
|
|
@ -8,23 +8,13 @@ import (
|
|||
_ "embed"
|
||||
|
||||
"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/httprate"
|
||||
"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.
|
||||
type Authenticator interface {
|
||||
jwtAuth
|
||||
|
@ -34,14 +24,16 @@ type Authenticator interface {
|
|||
type Auth struct {
|
||||
rl *httprate.RateLimiter
|
||||
jwt *jwtauth.JWTAuth
|
||||
ust users.Store
|
||||
cfg config.Auth
|
||||
}
|
||||
|
||||
// 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{
|
||||
rl: httprate.NewRateLimiter(5, time.Minute),
|
||||
cfg: cfg,
|
||||
ust: ust,
|
||||
}
|
||||
a.initJWT()
|
||||
|
||||
|
@ -63,7 +55,7 @@ var (
|
|||
// ErrorResponse writes the error and appropriate HTTP response code.
|
||||
func ErrorResponse(w http.ResponseWriter, err error) {
|
||||
switch err {
|
||||
case ErrLoginFailed, ErrUnauthorized:
|
||||
case ErrLoginFailed, ErrUnauthorized, rbac.ErrBadSubject:
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
case ErrBadRequest:
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
|
|
|
@ -4,17 +4,19 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"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/jwtauth/v5"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
|
@ -44,21 +46,16 @@ type jwtAuth 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)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
uidStr := tok.Subject()
|
||||
uidInt, err := strconv.Atoi(uidStr)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
username := tok.Subject()
|
||||
|
||||
uid := int32(uidInt)
|
||||
|
||||
return &uid
|
||||
return &username
|
||||
}
|
||||
|
||||
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 {
|
||||
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() {
|
||||
|
@ -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{
|
||||
"sub": strconv.Itoa(int(uid)),
|
||||
"sub": username,
|
||||
}
|
||||
jwtauth.SetExpiryIn(claims, time.Hour*24*30) // one month
|
||||
_, 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)
|
||||
return
|
||||
}
|
||||
existingSubjectUID := jwToken.Subject()
|
||||
if existingSubjectUID == "" {
|
||||
|
||||
existingSubjectUsername := jwToken.Subject()
|
||||
if existingSubjectUsername == "" {
|
||||
http.Error(w, "Invalid token", http.StatusBadRequest)
|
||||
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{
|
||||
Name: CookieName,
|
||||
|
|
|
@ -7,9 +7,10 @@ import (
|
|||
|
||||
"dynatron.me/x/stillbox/internal/audio"
|
||||
"dynatron.me/x/stillbox/internal/jsontypes"
|
||||
"dynatron.me/x/stillbox/pkg/auth"
|
||||
"dynatron.me/x/stillbox/pkg/pb"
|
||||
"dynatron.me/x/stillbox/pkg/rbac"
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups"
|
||||
"dynatron.me/x/stillbox/pkg/users"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
@ -52,27 +53,31 @@ type CallAudio struct {
|
|||
// further transformation. relayOut exists for compatibility with http
|
||||
// source CallUploadRequest as used in the relay sink.
|
||||
type Call struct {
|
||||
ID uuid.UUID `json:"id" relayOut:"id"`
|
||||
Audio []byte `json:"audio,omitempty" relayOut:"audio,omitempty" filenameField:"AudioName"`
|
||||
AudioName string `json:"audioName,omitempty" relayOut:"audioName,omitempty"`
|
||||
AudioType string `json:"audioType,omitempty" relayOut:"audioType,omitempty"`
|
||||
Duration CallDuration `json:"duration,omitempty" relayOut:"duration,omitempty"`
|
||||
DateTime time.Time `json:"call_date,omitempty" relayOut:"dateTime,omitempty"`
|
||||
Frequencies []int `json:"frequencies,omitempty" relayOut:"frequencies,omitempty"`
|
||||
Frequency int `json:"frequency,omitempty" relayOut:"frequency,omitempty"`
|
||||
Patches []int `json:"patches,omitempty" relayOut:"patches,omitempty"`
|
||||
Source int `json:"source,omitempty" relayOut:"source,omitempty"`
|
||||
System int `json:"system_id,omitempty" relayOut:"system,omitempty"`
|
||||
Submitter *auth.UserID `json:"submitter,omitempty" relayOut:"submitter,omitempty"`
|
||||
SystemLabel string `json:"system_name,omitempty" relayOut:"systemLabel,omitempty"`
|
||||
Talkgroup int `json:"tgid,omitempty" relayOut:"talkgroup,omitempty"`
|
||||
TalkgroupGroup *string `json:"talkgroupGroup,omitempty" relayOut:"talkgroupGroup,omitempty"`
|
||||
TalkgroupLabel *string `json:"talkgroupLabel,omitempty" relayOut:"talkgroupLabel,omitempty"`
|
||||
TGAlphaTag *string `json:"tg_name,omitempty" relayOut:"talkgroupTag,omitempty"`
|
||||
ID uuid.UUID `json:"id" relayOut:"id"`
|
||||
Audio []byte `json:"audio,omitempty" relayOut:"audio,omitempty" filenameField:"AudioName"`
|
||||
AudioName string `json:"audioName,omitempty" relayOut:"audioName,omitempty"`
|
||||
AudioType string `json:"audioType,omitempty" relayOut:"audioType,omitempty"`
|
||||
Duration CallDuration `json:"duration,omitempty" relayOut:"duration,omitempty"`
|
||||
DateTime time.Time `json:"call_date,omitempty" relayOut:"dateTime,omitempty"`
|
||||
Frequencies []int `json:"frequencies,omitempty" relayOut:"frequencies,omitempty"`
|
||||
Frequency int `json:"frequency,omitempty" relayOut:"frequency,omitempty"`
|
||||
Patches []int `json:"patches,omitempty" relayOut:"patches,omitempty"`
|
||||
Source int `json:"source,omitempty" relayOut:"source,omitempty"`
|
||||
System int `json:"system_id,omitempty" relayOut:"system,omitempty"`
|
||||
Submitter *users.UserID `json:"submitter,omitempty" relayOut:"submitter,omitempty"`
|
||||
SystemLabel string `json:"system_name,omitempty" relayOut:"systemLabel,omitempty"`
|
||||
Talkgroup int `json:"tgid,omitempty" relayOut:"talkgroup,omitempty"`
|
||||
TalkgroupGroup *string `json:"talkgroupGroup,omitempty" relayOut:"talkgroupGroup,omitempty"`
|
||||
TalkgroupLabel *string `json:"talkgroupLabel,omitempty" relayOut:"talkgroupLabel,omitempty"`
|
||||
TGAlphaTag *string `json:"tg_name,omitempty" relayOut:"talkgroupTag,omitempty"`
|
||||
|
||||
shouldStore bool `json:"-"`
|
||||
}
|
||||
|
||||
func (c *Call) GetResourceName() string {
|
||||
return rbac.ResourceCall
|
||||
}
|
||||
|
||||
func (c *Call) String() string {
|
||||
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/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/jackc/pgx/v5"
|
||||
|
@ -16,6 +19,12 @@ import (
|
|||
)
|
||||
|
||||
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(ctx context.Context, id uuid.UUID) (*calls.CallAudio, error)
|
||||
|
||||
|
@ -24,10 +33,13 @@ type Store interface {
|
|||
}
|
||||
|
||||
type store struct {
|
||||
db database.Store
|
||||
}
|
||||
|
||||
func NewStore() *store {
|
||||
return new(store)
|
||||
func NewStore(db database.Store) *store {
|
||||
return &store{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
type storeCtxKey string
|
||||
|
@ -41,13 +53,77 @@ func CtxWithStore(ctx context.Context, s Store) context.Context {
|
|||
func FromCtx(ctx context.Context) Store {
|
||||
s, ok := ctx.Value(StoreCtxKey).(Store)
|
||||
if !ok {
|
||||
return NewStore()
|
||||
panic("no call store in context")
|
||||
}
|
||||
|
||||
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) {
|
||||
_, err := rbac.Check(ctx, rbac.UseResource(rbac.ResourceCall), rbac.WithActions(rbac.ActionRead))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db := database.FromCtx(ctx)
|
||||
|
||||
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) {
|
||||
_, err = rbac.Check(ctx, rbac.UseResource(rbac.ResourceCall), rbac.WithActions(rbac.ActionRead))
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
db := database.FromCtx(ctx)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
SELECT
|
||||
c.call_date,
|
||||
|
@ -192,6 +201,17 @@ func (q *Queries) GetCallAudioByID(ctx context.Context, id uuid.UUID) (GetCallAu
|
|||
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
|
||||
SELECT pg_size_pretty(pg_database_size(current_database()))
|
||||
`
|
||||
|
|
|
@ -44,6 +44,7 @@ const createIncident = `-- name: CreateIncident :one
|
|||
INSERT INTO incidents (
|
||||
id,
|
||||
name,
|
||||
owner,
|
||||
description,
|
||||
start_time,
|
||||
end_time,
|
||||
|
@ -56,14 +57,16 @@ INSERT INTO incidents (
|
|||
$4,
|
||||
$5,
|
||||
$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 {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Owner int `json:"owner"`
|
||||
Description *string `json:"description"`
|
||||
StartTime pgtype.Timestamptz `json:"start_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,
|
||||
arg.ID,
|
||||
arg.Name,
|
||||
arg.Owner,
|
||||
arg.Description,
|
||||
arg.StartTime,
|
||||
arg.EndTime,
|
||||
|
@ -85,6 +89,7 @@ func (q *Queries) CreateIncident(ctx context.Context, arg CreateIncidentParams)
|
|||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.Owner,
|
||||
&i.Description,
|
||||
&i.StartTime,
|
||||
&i.EndTime,
|
||||
|
@ -107,6 +112,7 @@ const getIncident = `-- name: GetIncident :one
|
|||
SELECT
|
||||
i.id,
|
||||
i.name,
|
||||
i.owner,
|
||||
i.description,
|
||||
i.start_time,
|
||||
i.end_time,
|
||||
|
@ -122,6 +128,7 @@ func (q *Queries) GetIncident(ctx context.Context, id uuid.UUID) (Incident, erro
|
|||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.Owner,
|
||||
&i.Description,
|
||||
&i.StartTime,
|
||||
&i.EndTime,
|
||||
|
@ -237,6 +244,17 @@ func (q *Queries) GetIncidentCalls(ctx context.Context, id uuid.UUID) ([]GetInci
|
|||
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
|
||||
SELECT COUNT(*)
|
||||
FROM incidents i
|
||||
|
@ -262,6 +280,7 @@ const listIncidentsP = `-- name: ListIncidentsP :many
|
|||
SELECT
|
||||
i.id,
|
||||
i.name,
|
||||
i.owner,
|
||||
i.description,
|
||||
i.start_time,
|
||||
i.end_time,
|
||||
|
@ -299,6 +318,7 @@ type ListIncidentsPParams struct {
|
|||
type ListIncidentsPRow struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Owner int `json:"owner"`
|
||||
Description *string `json:"description"`
|
||||
StartTime pgtype.Timestamptz `json:"start_time"`
|
||||
EndTime pgtype.Timestamptz `json:"end_time"`
|
||||
|
@ -326,6 +346,7 @@ func (q *Queries) ListIncidentsP(ctx context.Context, arg ListIncidentsPParams)
|
|||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.Owner,
|
||||
&i.Description,
|
||||
&i.StartTime,
|
||||
&i.EndTime,
|
||||
|
@ -375,7 +396,7 @@ SET
|
|||
metadata = COALESCE($6, metadata)
|
||||
WHERE
|
||||
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 {
|
||||
|
@ -402,6 +423,7 @@ func (q *Queries) UpdateIncident(ctx context.Context, arg UpdateIncidentParams)
|
|||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.Owner,
|
||||
&i.Description,
|
||||
&i.StartTime,
|
||||
&i.EndTime,
|
||||
|
|
|
@ -502,6 +502,53 @@ func (_c *Store_CreatePartition_Call) RunAndReturn(run func(context.Context, str
|
|||
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
|
||||
func (_m *Store) CreateSystem(ctx context.Context, iD int, name string) error {
|
||||
ret := _m.Called(ctx, iD, name)
|
||||
|
@ -748,6 +795,53 @@ func (_c *Store_DeleteAPIKey_Call) RunAndReturn(run func(context.Context, string
|
|||
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
|
||||
func (_m *Store) DeleteIncident(ctx context.Context, id uuid.UUID) error {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
@ -795,6 +889,53 @@ func (_c *Store_DeleteIncident_Call) RunAndReturn(run func(context.Context, uuid
|
|||
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
|
||||
func (_m *Store) DeleteSystem(ctx context.Context, id int) error {
|
||||
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
|
||||
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)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetAPIKey")
|
||||
}
|
||||
|
||||
var r0 database.ApiKey
|
||||
var r0 database.GetAPIKeyRow
|
||||
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)
|
||||
}
|
||||
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)
|
||||
} 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 {
|
||||
|
@ -1079,12 +1220,12 @@ func (_c *Store_GetAPIKey_Call) Run(run func(ctx context.Context, apiKey string)
|
|||
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)
|
||||
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)
|
||||
return _c
|
||||
}
|
||||
|
@ -1264,6 +1405,65 @@ func (_c *Store_GetCallAudioByID_Call) RunAndReturn(run func(context.Context, uu
|
|||
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
|
||||
func (_m *Store) GetDatabaseSize(ctx context.Context) (string, error) {
|
||||
ret := _m.Called(ctx)
|
||||
|
@ -1436,6 +1636,120 @@ func (_c *Store_GetIncidentCalls_Call) RunAndReturn(run func(context.Context, uu
|
|||
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
|
||||
func (_m *Store) GetSystemName(ctx context.Context, systemID int) (string, error) {
|
||||
ret := _m.Called(ctx, systemID)
|
||||
|
@ -2433,63 +2747,6 @@ func (_c *Store_GetUserByID_Call) RunAndReturn(run func(context.Context, int) (d
|
|||
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
|
||||
func (_m *Store) GetUserByUsername(ctx context.Context, username string) (database.User, error) {
|
||||
ret := _m.Called(ctx, username)
|
||||
|
@ -2887,6 +3144,52 @@ func (_c *Store_ListIncidentsP_Call) RunAndReturn(run func(context.Context, data
|
|||
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
|
||||
func (_m *Store) RemoveFromIncident(ctx context.Context, iD uuid.UUID, callIds []uuid.UUID) error {
|
||||
ret := _m.Called(ctx, iD, callIds)
|
||||
|
@ -3505,6 +3808,65 @@ func (_c *Store_UpdateTalkgroup_Call) RunAndReturn(run func(context.Context, dat
|
|||
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
|
||||
func (_m *Store) UpsertTalkgroup(ctx context.Context, arg []database.UpsertTalkgroupParams) *database.UpsertTalkgroupBatchResults {
|
||||
ret := _m.Called(ctx, arg)
|
||||
|
|
|
@ -58,6 +58,7 @@ type Call struct {
|
|||
type Incident struct {
|
||||
ID uuid.UUID `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Owner int `json:"owner,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
StartTime pgtype.Timestamptz `json:"start_time,omitempty"`
|
||||
EndTime pgtype.Timestamptz `json:"end_time,omitempty"`
|
||||
|
@ -80,6 +81,14 @@ type Setting struct {
|
|||
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 {
|
||||
ID uuid.UUID `json:"id,omitempty"`
|
||||
Submitter *int32 `json:"submitter,omitempty"`
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
"dynatron.me/x/stillbox/internal/isoweek"
|
||||
"dynatron.me/x/stillbox/pkg/config"
|
||||
"dynatron.me/x/stillbox/pkg/database"
|
||||
"dynatron.me/x/stillbox/pkg/rbac"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
|
@ -134,6 +135,7 @@ func New(db database.Store, cfg config.Partition) (*partman, error) {
|
|||
var _ PartitionManager = (*partman)(nil)
|
||||
|
||||
func (pm *partman) Go(ctx context.Context) {
|
||||
ctx = rbac.CtxWithSubject(ctx, &rbac.SystemServiceSubject{Name: "partman"})
|
||||
tick := time.NewTicker(CheckInterval)
|
||||
|
||||
select {
|
||||
|
|
|
@ -19,20 +19,26 @@ type Querier interface {
|
|||
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)
|
||||
CreateIncident(ctx context.Context, arg CreateIncidentParams) (Incident, error)
|
||||
CreateShare(ctx context.Context, arg CreateShareParams) error
|
||||
CreateSystem(ctx context.Context, iD int, name string) error
|
||||
CreateUser(ctx context.Context, arg CreateUserParams) (User, error)
|
||||
DeleteAPIKey(ctx context.Context, apiKey string) error
|
||||
DeleteCall(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
|
||||
DeleteTalkgroup(ctx context.Context, systemID int32, tGID int32) 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)
|
||||
GetAppPrefs(ctx context.Context, appName string, uid int) ([]byte, 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)
|
||||
GetIncident(ctx context.Context, id uuid.UUID) (Incident, 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)
|
||||
GetTalkgroup(ctx context.Context, systemID int32, tGID int32) (GetTalkgroupRow, 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)
|
||||
GetTalkgroupsWithLearnedP(ctx context.Context, arg GetTalkgroupsWithLearnedPParams) ([]GetTalkgroupsWithLearnedPRow, 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)
|
||||
GetUsers(ctx context.Context) ([]User, error)
|
||||
ListCallsCount(ctx context.Context, arg ListCallsCountParams) (int64, error)
|
||||
ListCallsP(ctx context.Context, arg ListCallsPParams) ([]ListCallsPRow, error)
|
||||
ListIncidentsCount(ctx context.Context, start pgtype.Timestamptz, end pgtype.Timestamptz, filter *string) (int64, 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
|
||||
RestoreTalkgroupVersion(ctx context.Context, versionIds int) (Talkgroup, 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)
|
||||
UpdatePassword(ctx context.Context, username string, password string) 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
|
||||
}
|
||||
|
||||
|
|
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 (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"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
|
||||
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)
|
||||
var i ApiKey
|
||||
var i GetAPIKeyRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Owner,
|
||||
|
@ -104,6 +125,7 @@ func (q *Queries) GetAPIKey(ctx context.Context, apiKey string) (ApiKey, error)
|
|||
&i.Expires,
|
||||
&i.Disabled,
|
||||
&i.ApiKey,
|
||||
&i.Username,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
@ -121,7 +143,7 @@ func (q *Queries) GetAppPrefs(ctx context.Context, appName string, uid int) ([]b
|
|||
|
||||
const getUserByID = `-- name: GetUserByID :one
|
||||
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) {
|
||||
|
@ -138,28 +160,9 @@ func (q *Queries) GetUserByID(ctx context.Context, id int) (User, error) {
|
|||
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
|
||||
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) {
|
||||
|
@ -224,3 +227,26 @@ func (q *Queries) UpdatePassword(ctx context.Context, username string, password
|
|||
_, err := q.db.Exec(ctx, updatePassword, username, password)
|
||||
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/pkg/calls"
|
||||
"dynatron.me/x/stillbox/pkg/rbac"
|
||||
"dynatron.me/x/stillbox/pkg/users"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Incident struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Owner users.UserID `json:"owner"`
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
StartTime *jsontypes.Time `json:"startTime"`
|
||||
|
@ -19,6 +22,10 @@ type Incident struct {
|
|||
Calls []IncidentCall `json:"calls"`
|
||||
}
|
||||
|
||||
func (inc *Incident) GetResourceName() string {
|
||||
return rbac.ResourceIncident
|
||||
}
|
||||
|
||||
type IncidentCall struct {
|
||||
calls.Call
|
||||
Notes json.RawMessage `json:"notes"`
|
||||
|
|
|
@ -6,10 +6,11 @@ import (
|
|||
|
||||
"dynatron.me/x/stillbox/internal/common"
|
||||
"dynatron.me/x/stillbox/internal/jsontypes"
|
||||
"dynatron.me/x/stillbox/pkg/auth"
|
||||
"dynatron.me/x/stillbox/pkg/calls"
|
||||
"dynatron.me/x/stillbox/pkg/database"
|
||||
"dynatron.me/x/stillbox/pkg/incidents"
|
||||
"dynatron.me/x/stillbox/pkg/rbac"
|
||||
"dynatron.me/x/stillbox/pkg/users"
|
||||
"github.com/google/uuid"
|
||||
"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) {
|
||||
user, err := users.UserCheck(ctx, new(incidents.Incident), "create")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db := database.FromCtx(ctx)
|
||||
var dbInc database.Incident
|
||||
|
||||
|
@ -81,6 +87,7 @@ func (s *store) CreateIncident(ctx context.Context, inc incidents.Incident) (*in
|
|||
var err error
|
||||
dbInc, err = db.CreateIncident(ctx, database.CreateIncidentParams{
|
||||
ID: id,
|
||||
Owner: user.ID.Int(),
|
||||
Name: inc.Name,
|
||||
Description: inc.Description,
|
||||
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 {
|
||||
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 {
|
||||
if len(addCallIDs) > 0 {
|
||||
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) {
|
||||
_, err = rbac.Check(ctx, new(incidents.Incident), rbac.WithActions(rbac.ActionRead))
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
db := database.FromCtx(ctx)
|
||||
|
||||
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 {
|
||||
return incidents.Incident{
|
||||
ID: id,
|
||||
Owner: users.UserID(d.Owner),
|
||||
Name: d.Name,
|
||||
Description: d.Description,
|
||||
StartTime: jsontypes.TimePtrFromTSTZ(d.StartTime),
|
||||
|
@ -214,6 +236,7 @@ func fromDBListInPRow(id uuid.UUID, d database.ListIncidentsPRow) Incident {
|
|||
return Incident{
|
||||
Incident: incidents.Incident{
|
||||
ID: id,
|
||||
Owner: users.UserID(d.Owner),
|
||||
Name: d.Name,
|
||||
Description: d.Description,
|
||||
StartTime: jsontypes.TimePtrFromTSTZ(d.StartTime),
|
||||
|
@ -228,7 +251,7 @@ func fromDBCalls(d []database.GetIncidentCallsRow) []incidents.IncidentCall {
|
|||
r := make([]incidents.IncidentCall, 0, len(d))
|
||||
for _, v := range d {
|
||||
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{
|
||||
Call: calls.Call{
|
||||
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) {
|
||||
_, err := rbac.Check(ctx, new(incidents.Incident), rbac.WithActions(rbac.ActionRead))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var r incidents.Incident
|
||||
txErr := database.FromCtx(ctx).InTx(ctx, func(db database.Store) error {
|
||||
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) {
|
||||
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)
|
||||
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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/pb"
|
||||
"dynatron.me/x/stillbox/pkg/rbac"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
@ -38,6 +39,7 @@ func New() *Nexus {
|
|||
}
|
||||
|
||||
func (n *Nexus) Go(ctx context.Context) {
|
||||
ctx = rbac.CtxWithSubject(ctx, &rbac.SystemServiceSubject{Name: "nexus"})
|
||||
for {
|
||||
select {
|
||||
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"
|
||||
|
||||
"dynatron.me/x/stillbox/internal/common"
|
||||
"dynatron.me/x/stillbox/pkg/rbac"
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
@ -37,6 +38,7 @@ func (a *api) Subrouter() http.Handler {
|
|||
r.Mount("/call", new(callsAPI).Subrouter())
|
||||
r.Mount("/user", new(usersAPI).Subrouter())
|
||||
r.Mount("/incident", newIncidentsAPI(&a.baseURL).Subrouter())
|
||||
r.Mount("/share", newShareHandler(&a.baseURL).Subrouter())
|
||||
|
||||
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 {
|
||||
return &errResponse{
|
||||
Err: err,
|
||||
|
@ -127,9 +137,10 @@ var statusMapping = map[error]errResponder{
|
|||
ErrTGIDMismatch: badRequestErrText,
|
||||
ErrSysMismatch: badRequestErrText,
|
||||
tgstore.ErrReference: constraintErrText,
|
||||
ErrBadUID: unauthErrText,
|
||||
rbac.ErrBadSubject: unauthErrText,
|
||||
ErrBadAppName: unauthErrText,
|
||||
common.ErrPageOutOfRange: badRequestErrText,
|
||||
rbac.ErrNotAuthorized: unauthErrText,
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
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"
|
||||
|
||||
"dynatron.me/x/stillbox/pkg/auth"
|
||||
"dynatron.me/x/stillbox/pkg/rbac"
|
||||
"dynatron.me/x/stillbox/pkg/users"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrBadUID = errors.New("bad UID in token")
|
||||
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) {
|
||||
ctx := r.Context()
|
||||
|
||||
uid := auth.UIDFrom(ctx)
|
||||
username := auth.UsernameFrom(ctx)
|
||||
|
||||
if uid == nil {
|
||||
wErr(w, r, autoError(ErrBadUID))
|
||||
if username == nil {
|
||||
wErr(w, r, autoError(rbac.ErrBadSubject))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -55,7 +55,7 @@ func (ua *usersAPI) getPrefs(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
us := users.FromCtx(ctx)
|
||||
prefs, err := us.UserPrefs(ctx, *uid, *p.AppName)
|
||||
prefs, err := us.UserPrefs(ctx, *username, *p.AppName)
|
||||
if err != nil {
|
||||
wErr(w, r, autoError(err))
|
||||
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) {
|
||||
ctx := r.Context()
|
||||
|
||||
uid := auth.UIDFrom(ctx)
|
||||
username := auth.UsernameFrom(ctx)
|
||||
|
||||
if uid == nil {
|
||||
wErr(w, r, autoError(ErrBadUID))
|
||||
if username == nil {
|
||||
wErr(w, r, autoError(rbac.ErrBadSubject))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -102,7 +102,7 @@ func (ua *usersAPI) putPrefs(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
us := users.FromCtx(ctx)
|
||||
err = us.SetUserPrefs(ctx, *uid, *p.AppName, prefs)
|
||||
err = us.SetUserPrefs(ctx, *username, *p.AppName, prefs)
|
||||
if err != nil {
|
||||
wErr(w, r, autoError(err))
|
||||
return
|
||||
|
|
|
@ -7,5 +7,6 @@ import (
|
|||
)
|
||||
|
||||
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) {
|
||||
// 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))
|
||||
s.auth.PublicRoutes(r)
|
||||
// r.Mount("/share", s.share.ShareRouter(s.rest))
|
||||
})
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
|
@ -66,7 +68,7 @@ func (s *Server) setupRoutes() {
|
|||
func (s *Server) WithCtxStores() func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
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)
|
||||
}
|
||||
return http.HandlerFunc(fn)
|
||||
|
|
|
@ -15,7 +15,9 @@ import (
|
|||
"dynatron.me/x/stillbox/pkg/incidents/incstore"
|
||||
"dynatron.me/x/stillbox/pkg/nexus"
|
||||
"dynatron.me/x/stillbox/pkg/notify"
|
||||
"dynatron.me/x/stillbox/pkg/rbac"
|
||||
"dynatron.me/x/stillbox/pkg/rest"
|
||||
"dynatron.me/x/stillbox/pkg/share"
|
||||
"dynatron.me/x/stillbox/pkg/sinks"
|
||||
"dynatron.me/x/stillbox/pkg/sources"
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
|
||||
|
@ -48,6 +50,8 @@ type Server struct {
|
|||
users users.Store
|
||||
calls callstore.Store
|
||||
incidents incstore.Store
|
||||
share share.Service
|
||||
rbac rbac.RBAC
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
authenticator := auth.NewAuthenticator(cfg.Auth)
|
||||
ust := users.NewStore(db)
|
||||
|
||||
authenticator := auth.NewAuthenticator(cfg.Auth, ust)
|
||||
|
||||
notifier, err := notify.New(cfg.Notify)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tgCache := tgstore.NewCache()
|
||||
tgCache := tgstore.NewCache(db)
|
||||
api := rest.New(cfg.BaseURL.URL())
|
||||
|
||||
rbacSvc, err := rbac.New()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
srv := &Server{
|
||||
auth: authenticator,
|
||||
conf: cfg,
|
||||
|
@ -85,9 +96,11 @@ func New(ctx context.Context, cfg *config.Configuration) (*Server, error) {
|
|||
tgs: tgCache,
|
||||
sinks: sinks.NewSinkManager(),
|
||||
rest: api,
|
||||
users: users.NewStore(),
|
||||
calls: callstore.NewStore(),
|
||||
share: share.NewService(),
|
||||
users: ust,
|
||||
calls: callstore.NewStore(db),
|
||||
incidents: incstore.NewStore(),
|
||||
rbac: rbacSvc,
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
if srv.alerter.Enabled() {
|
||||
|
@ -135,12 +148,14 @@ func New(ctx context.Context, cfg *config.Configuration) (*Server, error) {
|
|||
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 = tgstore.CtxWithStore(ctx, s.tgs)
|
||||
ctx = users.CtxWithStore(ctx, s.users)
|
||||
ctx = callstore.CtxWithStore(ctx, s.calls)
|
||||
ctx = incstore.CtxWithStore(ctx, s.incidents)
|
||||
ctx = share.CtxWithStore(ctx, s.share.ShareStore())
|
||||
ctx = rbac.CtxWithRBAC(ctx, s.rbac)
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
@ -150,7 +165,7 @@ func (s *Server) Go(ctx context.Context) error {
|
|||
|
||||
s.installHupHandler()
|
||||
|
||||
ctx = s.addStoresTo(ctx)
|
||||
ctx = s.fillCtx(ctx)
|
||||
|
||||
httpSrv := &http.Server{
|
||||
Addr: s.conf.Listen,
|
||||
|
@ -159,6 +174,7 @@ func (s *Server) Go(ctx context.Context) error {
|
|||
|
||||
go s.nex.Go(ctx)
|
||||
go s.alerter.Go(ctx)
|
||||
go s.share.Go(ctx)
|
||||
|
||||
if pm := s.partman; pm != nil {
|
||||
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 (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"dynatron.me/x/stillbox/internal/common"
|
||||
"dynatron.me/x/stillbox/pkg/calls"
|
||||
"dynatron.me/x/stillbox/pkg/calls/callstore"
|
||||
"dynatron.me/x/stillbox/pkg/database"
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
|
@ -29,59 +26,9 @@ func (s *DatabaseSink) Call(ctx context.Context, call *calls.Call) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
params := s.toAddCallParams(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
|
||||
return callstore.FromCtx(ctx).AddCall(ctx, call)
|
||||
}
|
||||
|
||||
func (s *DatabaseSink) SinkType() string {
|
||||
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/forms"
|
||||
"dynatron.me/x/stillbox/pkg/auth"
|
||||
"dynatron.me/x/stillbox/pkg/calls"
|
||||
"dynatron.me/x/stillbox/pkg/config"
|
||||
"dynatron.me/x/stillbox/pkg/sources"
|
||||
"dynatron.me/x/stillbox/pkg/users"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
@ -32,16 +32,16 @@ func TestRelay(t *testing.T) {
|
|||
|
||||
tests := []struct {
|
||||
name string
|
||||
submitter auth.UserID
|
||||
submitter users.UserID
|
||||
apiKey string
|
||||
call calls.Call
|
||||
}{
|
||||
{
|
||||
name: "base",
|
||||
submitter: auth.UserID(1),
|
||||
submitter: users.UserID(1),
|
||||
call: calls.Call{
|
||||
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,
|
||||
Talkgroup: 10101,
|
||||
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/pkg/auth"
|
||||
"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/rs/zerolog/log"
|
||||
)
|
||||
|
@ -70,7 +72,7 @@ func (car *CallUploadRequest) mimeType() string {
|
|||
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{
|
||||
Submitter: &submitter,
|
||||
System: car.System,
|
||||
|
@ -98,7 +100,13 @@ func (h *RdioHTTP) routeCallUpload(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
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 {
|
||||
auth.ErrorResponse(w, err)
|
||||
return
|
||||
|
@ -117,20 +125,22 @@ func (h *RdioHTTP) routeCallUpload(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
call, err := cur.ToCall(*submitter)
|
||||
call, err := cur.ToCall(submitter.ID)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("toCall failed")
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err = h.ing.Ingest(ctx, call)
|
||||
err = h.ing.Ingest(rbac.CtxWithSubject(ctx, submitterSub), call)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("ingest failed")
|
||||
http.Error(w, "Call ingest failed.", http.StatusInternalServerError)
|
||||
if rbac.ErrAccessDenied(err) != nil {
|
||||
log.Error().Err(err).Msg("ingest failed")
|
||||
http.Error(w, "Call ingest failed.", http.StatusForbidden)
|
||||
}
|
||||
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."))
|
||||
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"
|
||||
|
||||
"dynatron.me/x/stillbox/pkg/database"
|
||||
"dynatron.me/x/stillbox/pkg/rbac"
|
||||
)
|
||||
|
||||
type Talkgroup struct {
|
||||
|
@ -17,6 +18,10 @@ type Talkgroup struct {
|
|||
Learned bool `json:"learned"`
|
||||
}
|
||||
|
||||
func (t *Talkgroup) GetResourceName() string {
|
||||
return rbac.ResourceTalkgroup
|
||||
}
|
||||
|
||||
func (t Talkgroup) String() string {
|
||||
if t.System.Name == "" {
|
||||
t.System.Name = strconv.Itoa(int(t.Talkgroup.TGID))
|
||||
|
|
|
@ -8,11 +8,12 @@ import (
|
|||
"time"
|
||||
|
||||
"dynatron.me/x/stillbox/internal/common"
|
||||
"dynatron.me/x/stillbox/pkg/auth"
|
||||
"dynatron.me/x/stillbox/pkg/calls"
|
||||
"dynatron.me/x/stillbox/pkg/config"
|
||||
"dynatron.me/x/stillbox/pkg/database"
|
||||
"dynatron.me/x/stillbox/pkg/rbac"
|
||||
tgsp "dynatron.me/x/stillbox/pkg/talkgroups"
|
||||
"dynatron.me/x/stillbox/pkg/users"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
@ -176,7 +177,7 @@ func CtxWithStore(ctx context.Context, s Store) context.Context {
|
|||
func FromCtx(ctx context.Context) Store {
|
||||
s, ok := ctx.Value(StoreCtxKey).(Store)
|
||||
if !ok {
|
||||
return NewCache()
|
||||
panic("no tg store in context")
|
||||
}
|
||||
|
||||
return s
|
||||
|
@ -201,19 +202,23 @@ type cache struct {
|
|||
sync.RWMutex
|
||||
tgs tgMap
|
||||
systems map[int]string
|
||||
db database.Store
|
||||
}
|
||||
|
||||
// NewCache returns a new cache Store.
|
||||
func NewCache() *cache {
|
||||
func NewCache(db database.Store) *cache {
|
||||
tgc := &cache{
|
||||
tgs: make(tgMap),
|
||||
systems: make(map[int]string),
|
||||
db: db,
|
||||
}
|
||||
|
||||
return tgc
|
||||
}
|
||||
|
||||
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 {
|
||||
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) {
|
||||
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))
|
||||
opt := sOpt(opts)
|
||||
var err error
|
||||
if tgs != nil {
|
||||
toGet := make(tgsp.IDs, 0, len(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 {
|
||||
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 {
|
||||
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) {
|
||||
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)
|
||||
var err error
|
||||
if opt.pagination != nil {
|
||||
sortDir, err := opt.pagination.SortDir()
|
||||
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) {
|
||||
_, err := rbac.Check(ctx, rbac.UseResource(rbac.ResourceTalkgroup), rbac.WithActions(rbac.ActionRead))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rec, has := t.get(tg)
|
||||
|
||||
if has {
|
||||
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 {
|
||||
case nil:
|
||||
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) {
|
||||
_, err := rbac.Check(ctx, rbac.UseResource(rbac.ResourceTalkgroup), rbac.WithActions(rbac.ActionRead))
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
t.RLock()
|
||||
n, has := t.systems[id]
|
||||
t.RUnlock()
|
||||
|
||||
if !has {
|
||||
sys, err := database.FromCtx(ctx).GetSystemName(ctx, id)
|
||||
sys, err := t.db.GetSystemName(ctx, id)
|
||||
if err != nil {
|
||||
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) {
|
||||
user, err := users.UserCheck(ctx, new(tgsp.Talkgroup), "update")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sysName, has := t.SystemName(ctx, int(*input.SystemID))
|
||||
if !has {
|
||||
return nil, ErrNoSuchSystem
|
||||
}
|
||||
db := database.FromCtx(ctx)
|
||||
|
||||
db := t.db
|
||||
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
|
||||
tg, oerr = db.UpdateTalkgroup(ctx, input)
|
||||
if oerr != nil {
|
||||
return oerr
|
||||
}
|
||||
versionBatch := db.StoreTGVersion(ctx, []database.StoreTGVersionParams{{
|
||||
Submitter: auth.UIDFrom(ctx),
|
||||
Submitter: user.ID.Int32Ptr(),
|
||||
TGID: *input.TGID,
|
||||
}})
|
||||
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 {
|
||||
_, err := rbac.Check(ctx, rbac.UseResource(rbac.ResourceTalkgroup), rbac.WithActions(rbac.ActionDelete))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.Lock()
|
||||
defer t.Unlock()
|
||||
|
||||
t.invalidate()
|
||||
|
||||
err := database.FromCtx(ctx).DeleteSystem(ctx, id)
|
||||
err = t.db.DeleteSystem(ctx, id)
|
||||
switch {
|
||||
case err == 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 {
|
||||
_, err := rbac.Check(ctx, rbac.UseResource(rbac.ResourceTalkgroup), rbac.WithActions(rbac.ActionDelete))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.Lock()
|
||||
defer t.Unlock()
|
||||
|
||||
err := database.FromCtx(ctx).InTx(ctx, func(db database.Store) error {
|
||||
err := db.StoreDeletedTGVersion(ctx, common.PtrTo(int32(id.System)), common.PtrTo(int32(id.Talkgroup)), auth.UIDFrom(ctx))
|
||||
user, err := users.UserCheck(ctx, new(tgsp.Talkgroup), "update")
|
||||
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 {
|
||||
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) {
|
||||
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)
|
||||
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) {
|
||||
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)
|
||||
if !hasSys {
|
||||
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))
|
||||
|
||||
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))
|
||||
for i := range input {
|
||||
// normalize tags
|
||||
|
@ -670,7 +725,7 @@ func (t *cache) UpsertTGs(ctx context.Context, system int, input []database.Upse
|
|||
versionParams = append(versionParams, database.StoreTGVersionParams{
|
||||
SystemID: int32(system),
|
||||
TGID: r.TGID,
|
||||
Submitter: auth.UIDFrom(ctx),
|
||||
Submitter: user.ID.Int32Ptr(),
|
||||
})
|
||||
tgs = append(tgs, &tgsp.Talkgroup{
|
||||
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 {
|
||||
_, err := rbac.Check(ctx, rbac.UseResource(rbac.ResourceTalkgroup), rbac.WithActions(rbac.ActionCreate))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.Lock()
|
||||
defer t.Unlock()
|
||||
|
||||
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) {
|
||||
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/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/tgstore"
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups/xport"
|
||||
"dynatron.me/x/stillbox/pkg/users"
|
||||
)
|
||||
|
||||
func getFixture(fixture string) []byte {
|
||||
|
@ -51,14 +54,19 @@ func TestRadioRef(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
subject := users.User{IsAdmin: true}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.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 {
|
||||
dbMock.EXPECT().GetSystemName(mock.AnythingOfType("*context.valueCtx"), tc.sysID).Return(tc.sysName, nil)
|
||||
}
|
||||
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{
|
||||
Type: xport.Format(tc.impType),
|
||||
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 (
|
||||
"context"
|
||||
|
||||
"dynatron.me/x/stillbox/internal/cache"
|
||||
"dynatron.me/x/stillbox/pkg/database"
|
||||
)
|
||||
|
||||
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(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(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 {
|
||||
return new(store)
|
||||
func NewStore(db database.Store) *postgresStore {
|
||||
return &postgresStore{
|
||||
Cache: cache.New[string, *User](),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
type storeCtxKey string
|
||||
|
@ -32,16 +50,56 @@ func CtxWithStore(ctx context.Context, s Store) context.Context {
|
|||
func FromCtx(ctx context.Context) Store {
|
||||
s, ok := ctx.Value(StoreCtxKey).(Store)
|
||||
if !ok {
|
||||
return NewStore()
|
||||
panic("no users store in context")
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *store) UserPrefs(ctx context.Context, uid int32, appName string) ([]byte, error) {
|
||||
db := database.FromCtx(ctx)
|
||||
func (s *postgresStore) Invalidate() {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -49,8 +107,15 @@ func (s *store) UserPrefs(ctx context.Context, uid int32, appName string) ([]byt
|
|||
return []byte(prefs), err
|
||||
}
|
||||
|
||||
func (s *store) SetUserPrefs(ctx context.Context, uid int32, appName string, prefs []byte) error {
|
||||
db := database.FromCtx(ctx)
|
||||
func (s *postgresStore) SetUserPrefs(ctx context.Context, username string, appName string, prefs []byte) error {
|
||||
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(
|
||||
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,
|
||||
password TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
|
@ -141,6 +141,7 @@ CREATE TABLE IF NOT EXISTS settings(
|
|||
CREATE TABLE IF NOT EXISTS incidents(
|
||||
id UUID PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
owner INTEGER NOT NULL,
|
||||
description TEXT,
|
||||
start_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),
|
||||
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
|
||||
) 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 (
|
||||
id,
|
||||
name,
|
||||
owner,
|
||||
description,
|
||||
start_time,
|
||||
end_time,
|
||||
|
@ -41,6 +42,7 @@ INSERT INTO incidents (
|
|||
) VALUES (
|
||||
@id,
|
||||
@name,
|
||||
@owner,
|
||||
sqlc.narg('description'),
|
||||
sqlc.narg('start_time'),
|
||||
sqlc.narg('end_time'),
|
||||
|
@ -54,6 +56,7 @@ RETURNING *;
|
|||
SELECT
|
||||
i.id,
|
||||
i.name,
|
||||
i.owner,
|
||||
i.description,
|
||||
i.start_time,
|
||||
i.end_time,
|
||||
|
@ -148,6 +151,7 @@ ORDER BY ic.call_date ASC;
|
|||
SELECT
|
||||
i.id,
|
||||
i.name,
|
||||
i.owner,
|
||||
i.description,
|
||||
i.start_time,
|
||||
i.end_time,
|
||||
|
@ -171,3 +175,6 @@ RETURNING *;
|
|||
|
||||
-- name: DeleteIncident :exec
|
||||
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
|
||||
SELECT * FROM users
|
||||
WHERE id = $1 LIMIT 1;
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: GetUserByUsername :one
|
||||
SELECT * FROM users
|
||||
WHERE username = $1 LIMIT 1;
|
||||
|
||||
-- name: GetUserByUID :one
|
||||
SELECT * FROM users
|
||||
WHERE id = $1 LIMIT 1;
|
||||
WHERE username = $1;
|
||||
|
||||
-- name: GetUsers :many
|
||||
SELECT * FROM users;
|
||||
|
@ -28,6 +24,14 @@ DELETE FROM users WHERE username = $1;
|
|||
-- name: UpdatePassword :exec
|
||||
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
|
||||
INSERT INTO api_keys(
|
||||
owner,
|
||||
|
@ -42,7 +46,17 @@ RETURNING *;
|
|||
DELETE FROM api_keys WHERE api_key = $1;
|
||||
|
||||
-- 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
|
||||
SELECT (prefs->>(@app_name::TEXT))::JSONB FROM users WHERE id = @uid;
|
||||
|
|
Loading…
Add table
Reference in a new issue