diff --git a/go.mod b/go.mod index 29b60a3..a766b05 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 9228be0..26398c9 100644 --- a/go.sum +++ b/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= @@ -157,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= @@ -183,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= @@ -194,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= @@ -208,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= diff --git a/internal/forms/marshal_test.go b/internal/forms/marshal_test.go index ae624c2..eb5eaba 100644 --- a/internal/forms/marshal_test.go +++ b/internal/forms/marshal_test.go @@ -18,6 +18,7 @@ import ( "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 +63,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), diff --git a/pkg/auth/apikey.go b/pkg/auth/apikey.go index d18a303..4bc86f3 100644 --- a/pkg/auth/apikey.go +++ b/pkg/auth/apikey.go @@ -7,18 +7,19 @@ import ( "time" "dynatron.me/x/stillbox/pkg/database" + "dynatron.me/x/stillbox/pkg/users" "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) (*users.UserID, error) } -func (a *Auth) CheckAPIKey(ctx context.Context, key string) (*UserID, error) { +func (a *Auth) CheckAPIKey(ctx context.Context, key string) (*users.UserID, error) { keyUuid, err := uuid.Parse(key) if err != nil { log.Error().Str("apikey", key).Msg("cannot parse key") @@ -44,7 +45,7 @@ func (a *Auth) CheckAPIKey(ctx context.Context, key string) (*UserID, error) { return nil, ErrUnauthorized } - owner := UserID(apik.Owner) + owner := users.UserID(apik.Owner) return &owner, nil } diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index e7bd43c..67460c9 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -13,18 +13,6 @@ import ( "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 diff --git a/pkg/auth/jwt.go b/pkg/auth/jwt.go index 99c2ba7..289120e 100644 --- a/pkg/auth/jwt.go +++ b/pkg/auth/jwt.go @@ -11,6 +11,7 @@ import ( "golang.org/x/crypto/bcrypt" "dynatron.me/x/stillbox/pkg/database" + "dynatron.me/x/stillbox/pkg/users" "github.com/go-chi/chi/v5" "github.com/go-chi/jwtauth/v5" @@ -44,7 +45,8 @@ type jwtAuth interface { type claims map[string]interface{} -func UIDFrom(ctx context.Context) *int32 { +// TODO: change this to UserFrom() *users.User +func UIDFrom(ctx context.Context) *users.UserID { tok, _, err := jwtauth.FromContext(ctx) if err != nil { return nil @@ -56,7 +58,7 @@ func UIDFrom(ctx context.Context) *int32 { return nil } - uid := int32(uidInt) + uid := users.UserID(int32(uidInt)) return &uid } diff --git a/pkg/calls/call.go b/pkg/calls/call.go index effdbce..3d46fa6 100644 --- a/pkg/calls/call.go +++ b/pkg/calls/call.go @@ -7,9 +7,9 @@ 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/talkgroups" + "dynatron.me/x/stillbox/pkg/users" "github.com/google/uuid" "google.golang.org/protobuf/types/known/timestamppb" @@ -52,23 +52,23 @@ 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:"-"` } diff --git a/pkg/database/incidents.sql.go b/pkg/database/incidents.sql.go index d422db0..18f6a9d 100644 --- a/pkg/database/incidents.sql.go +++ b/pkg/database/incidents.sql.go @@ -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, @@ -262,6 +269,7 @@ const listIncidentsP = `-- name: ListIncidentsP :many SELECT i.id, i.name, + i.owner, i.description, i.start_time, i.end_time, @@ -299,6 +307,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 +335,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 +385,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 +412,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, diff --git a/pkg/database/models.go b/pkg/database/models.go index c1e75da..5080c95 100644 --- a/pkg/database/models.go +++ b/pkg/database/models.go @@ -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"` diff --git a/pkg/incidents/incstore/store.go b/pkg/incidents/incstore/store.go index 0a4ba6e..019f59c 100644 --- a/pkg/incidents/incstore/store.go +++ b/pkg/incidents/incstore/store.go @@ -10,6 +10,8 @@ import ( "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" ) @@ -75,12 +77,19 @@ func (s *store) CreateIncident(ctx context.Context, inc incidents.Incident) (*in db := database.FromCtx(ctx) var dbInc database.Incident + // TODO: replace this with a real RBAC check + owner := auth.UIDFrom(ctx) + if owner == nil { + return nil, rbac.ErrNotAuthorized + } + id := uuid.New() txErr := db.InTx(ctx, func(db database.Store) error { var err error dbInc, err = db.CreateIncident(ctx, database.CreateIncidentParams{ ID: id, + Owner: owner.Int(), Name: inc.Name, Description: inc.Description, StartTime: inc.StartTime.PGTypeTSTZ(), @@ -228,7 +237,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, diff --git a/pkg/rbac/rbac.go b/pkg/rbac/rbac.go index 0b31261..5c75598 100644 --- a/pkg/rbac/rbac.go +++ b/pkg/rbac/rbac.go @@ -1 +1,26 @@ package rbac + +import ( + "errors" + + "github.com/el-mike/restrict/v2" +) + +var ( + ErrNotAuthorized = errors.New("not authorized") +) + +var policy = &restrict.PolicyDefinition{ + Roles: restrict.Roles{ + "User": { + Grants: restrict.GrantsMap{ + "Conversation": { + &restrict.Permission{Action: "read"}, + &restrict.Permission{Action: "create"}, + }, + }, + }, + "Guest": {}, + "Admin": {}, + }, +} diff --git a/pkg/rest/api.go b/pkg/rest/api.go index 70c2881..fbd2bae 100644 --- a/pkg/rest/api.go +++ b/pkg/rest/api.go @@ -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" @@ -131,6 +132,7 @@ var statusMapping = map[error]errResponder{ ErrBadUID: unauthErrText, ErrBadAppName: unauthErrText, common.ErrPageOutOfRange: badRequestErrText, + rbac.ErrNotAuthorized: unauthErrText, } func autoError(err error) render.Renderer { diff --git a/pkg/server/routes.go b/pkg/server/routes.go index a7f0788..2300c04 100644 --- a/pkg/server/routes.go +++ b/pkg/server/routes.go @@ -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) { diff --git a/pkg/share/service.go b/pkg/share/service.go index 8a08f0d..b4a8c87 100644 --- a/pkg/share/service.go +++ b/pkg/share/service.go @@ -30,7 +30,7 @@ func (s *service) Go(ctx context.Context) { for { select { - case <- tick.C: + case <-tick.C: err := s.store.Prune(ctx) if err != nil { log.Error().Err(err).Msg("share prune failed") diff --git a/pkg/share/share.go b/pkg/share/share.go index 912ab58..8865d71 100644 --- a/pkg/share/share.go +++ b/pkg/share/share.go @@ -18,15 +18,15 @@ type EntityType string const ( EntityIncident EntityType = "incident" - EntityCall EntityType = "call" + 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"` + ID string `json:"id"` + Type EntityType `json:"entityType"` + EntityID uuid.UUID `json:"entityID"` Expiration *jsontypes.Time `json:"expiration"` } @@ -46,9 +46,9 @@ func (s *service) NewShare(ctx context.Context, shType EntityType, shID uuid.UUI } share := &Share{ - ID: id, - Type: shType, - EntityID: shID, + ID: id, + Type: shType, + EntityID: shID, Expiration: expT, } diff --git a/pkg/share/store.go b/pkg/share/store.go index 8c75d66..4ee6ec8 100644 --- a/pkg/share/store.go +++ b/pkg/share/store.go @@ -26,9 +26,9 @@ type postgresStore struct { func recToShare(share database.Share) *Share { return &Share{ - ID: share.ID, - Type: EntityType(share.EntityType), - EntityID: share.EntityID, + ID: share.ID, + Type: EntityType(share.EntityType), + EntityID: share.EntityID, Expiration: jsontypes.TimePtrFromTSTZ(share.Expiration), } } @@ -46,9 +46,9 @@ func (s *postgresStore) Get(ctx context.Context, id string) (*Share, error) { func (s *postgresStore) Create(ctx context.Context, share *Share) error { db := database.FromCtx(ctx) err := db.CreateShare(ctx, database.CreateShareParams{ - ID: share.ID, + ID: share.ID, EntityType: string(share.Type), - EntityID: share.EntityID, + EntityID: share.EntityID, Expiration: share.Expiration.PGTypeTSTZ(), }) diff --git a/pkg/sinks/relay_test.go b/pkg/sinks/relay_test.go index b99d03b..d0891c0 100644 --- a/pkg/sinks/relay_test.go +++ b/pkg/sinks/relay_test.go @@ -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), diff --git a/pkg/sources/http.go b/pkg/sources/http.go index 1c18b39..503881e 100644 --- a/pkg/sources/http.go +++ b/pkg/sources/http.go @@ -9,6 +9,7 @@ import ( "dynatron.me/x/stillbox/internal/forms" "dynatron.me/x/stillbox/pkg/auth" "dynatron.me/x/stillbox/pkg/calls" + "dynatron.me/x/stillbox/pkg/users" "github.com/go-chi/chi/v5" "github.com/rs/zerolog/log" ) @@ -70,7 +71,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, diff --git a/pkg/talkgroups/tgstore/store.go b/pkg/talkgroups/tgstore/store.go index cf45be8..896cb3f 100644 --- a/pkg/talkgroups/tgstore/store.go +++ b/pkg/talkgroups/tgstore/store.go @@ -528,7 +528,7 @@ func (t *cache) UpdateTG(ctx context.Context, input database.UpdateTalkgroupPara return oerr } versionBatch := db.StoreTGVersion(ctx, []database.StoreTGVersionParams{{ - Submitter: auth.UIDFrom(ctx), + Submitter: auth.UIDFrom(ctx).Int32Ptr(), TGID: *input.TGID, }}) defer versionBatch.Close() @@ -578,7 +578,7 @@ func (t *cache) DeleteTG(ctx context.Context, id tgsp.ID) error { 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)) + err := db.StoreDeletedTGVersion(ctx, common.PtrTo(int32(id.System)), common.PtrTo(int32(id.Talkgroup)), auth.UIDFrom(ctx).Int32Ptr()) if err != nil { return err } @@ -670,7 +670,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: auth.UIDFrom(ctx).Int32Ptr(), }) tgs = append(tgs, &tgsp.Talkgroup{ Talkgroup: r, diff --git a/pkg/users/store.go b/pkg/users/store.go index b37384c..bfdfde1 100644 --- a/pkg/users/store.go +++ b/pkg/users/store.go @@ -14,11 +14,11 @@ type Store interface { SetUserPrefs(ctx context.Context, uid int32, appName string, prefs []byte) error } -type store struct { +type postgresStore struct { } -func NewStore() *store { - return new(store) +func NewStore() *postgresStore { + return new(postgresStore) } type storeCtxKey string @@ -38,7 +38,7 @@ func FromCtx(ctx context.Context) Store { return s } -func (s *store) UserPrefs(ctx context.Context, uid int32, appName string) ([]byte, error) { +func (s *postgresStore) UserPrefs(ctx context.Context, uid int32, appName string) ([]byte, error) { db := database.FromCtx(ctx) prefs, err := db.GetAppPrefs(ctx, appName, int(uid)) @@ -49,8 +49,10 @@ 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 { +func (s *postgresStore) SetUserPrefs(ctx context.Context, uid int32, appName string, prefs []byte) error { db := database.FromCtx(ctx) return db.SetAppPrefs(ctx, appName, prefs, int(uid)) } + +//func (s *postgresStore) diff --git a/pkg/users/user.go b/pkg/users/user.go new file mode 100644 index 0000000..82c1fb3 --- /dev/null +++ b/pkg/users/user.go @@ -0,0 +1,30 @@ +package users + +import ( + "encoding/json" +) + +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) +} + +type User struct { + ID UserID + Username string + Password string + Email string + IsAdmin bool + Prefs json.RawMessage +} diff --git a/sql/postgres/migrations/001_initial.up.sql b/sql/postgres/migrations/001_initial.up.sql index 6657f60..f7889cb 100644 --- a/sql/postgres/migrations/001_initial.up.sql +++ b/sql/postgres/migrations/001_initial.up.sql @@ -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, diff --git a/sql/postgres/queries/incidents.sql b/sql/postgres/queries/incidents.sql index 214a123..314f64d 100644 --- a/sql/postgres/queries/incidents.sql +++ b/sql/postgres/queries/incidents.sql @@ -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,