WIP cycle

This commit is contained in:
Daniel Ponte 2025-01-20 22:38:27 -05:00
parent b171e8431a
commit 5ff3066d6d
16 changed files with 287 additions and 172 deletions

View file

@ -8,7 +8,6 @@ import (
"dynatron.me/x/stillbox/internal/audio" "dynatron.me/x/stillbox/internal/audio"
"dynatron.me/x/stillbox/internal/jsontypes" "dynatron.me/x/stillbox/internal/jsontypes"
"dynatron.me/x/stillbox/pkg/pb" "dynatron.me/x/stillbox/pkg/pb"
"dynatron.me/x/stillbox/pkg/rbac"
"dynatron.me/x/stillbox/pkg/talkgroups" "dynatron.me/x/stillbox/pkg/talkgroups"
"dynatron.me/x/stillbox/pkg/users" "dynatron.me/x/stillbox/pkg/users"
@ -16,6 +15,8 @@ import (
"google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/timestamppb"
) )
const Resource = "call"
type CallDuration time.Duration type CallDuration time.Duration
func (d CallDuration) Duration() time.Duration { func (d CallDuration) Duration() time.Duration {
@ -76,7 +77,7 @@ type Call struct {
} }
func (c *Call) GetResourceName() string { func (c *Call) GetResourceName() string {
return rbac.ResourceCall return Resource
} }
func (c *Call) String() string { func (c *Call) String() string {

View file

@ -40,6 +40,21 @@ func (q *Queries) AddToIncident(ctx context.Context, incidentID uuid.UUID, callI
return err return err
} }
const callInIncident = `-- name: CallInIncident :one
SELECT EXISTS
(SELECT 1 FROM incidents_calls ic
WHERE
ic.incident_id = $1 AND
ic.call_id = $2)
`
func (q *Queries) CallInIncident(ctx context.Context, incidentID uuid.UUID, callID uuid.UUID) (bool, error) {
row := q.db.QueryRow(ctx, callInIncident, incidentID, callID)
var exists bool
err := row.Scan(&exists)
return exists, err
}
const createIncident = `-- name: CreateIncident :one const createIncident = `-- name: CreateIncident :one
INSERT INTO incidents ( INSERT INTO incidents (
id, id,

View file

@ -14,150 +14,150 @@ import (
) )
type Alert struct { type Alert struct {
ID int `json:"id,omitempty"` ID int `json:"id"`
Time pgtype.Timestamptz `json:"time,omitempty"` Time pgtype.Timestamptz `json:"time"`
TGID int `json:"tgid,omitempty"` TGID int `json:"tgid"`
SystemID int `json:"system_id,omitempty"` SystemID int `json:"system_id"`
Weight *float32 `json:"weight,omitempty"` Weight *float32 `json:"weight"`
Score *float32 `json:"score,omitempty"` Score *float32 `json:"score"`
OrigScore *float32 `json:"orig_score,omitempty"` OrigScore *float32 `json:"orig_score"`
Notified bool `json:"notified,omitempty"` Notified bool `json:"notified"`
Metadata []byte `json:"metadata,omitempty"` Metadata []byte `json:"metadata"`
} }
type ApiKey struct { type ApiKey struct {
ID int `json:"id,omitempty"` ID int `json:"id"`
Owner int `json:"owner,omitempty"` Owner int `json:"owner"`
CreatedAt time.Time `json:"created_at,omitempty"` CreatedAt time.Time `json:"created_at"`
Expires pgtype.Timestamp `json:"expires,omitempty"` Expires pgtype.Timestamp `json:"expires"`
Disabled *bool `json:"disabled,omitempty"` Disabled *bool `json:"disabled"`
ApiKey string `json:"api_key,omitempty"` ApiKey string `json:"api_key"`
} }
type Call struct { type Call struct {
ID uuid.UUID `json:"id,omitempty"` ID uuid.UUID `json:"id"`
Submitter *int32 `json:"submitter,omitempty"` Submitter *int32 `json:"submitter"`
System int `json:"system,omitempty"` System int `json:"system"`
Talkgroup int `json:"talkgroup,omitempty"` Talkgroup int `json:"talkgroup"`
CallDate pgtype.Timestamptz `json:"call_date,omitempty"` CallDate pgtype.Timestamptz `json:"call_date"`
AudioName *string `json:"audio_name,omitempty"` AudioName *string `json:"audio_name"`
AudioBlob []byte `json:"audio_blob,omitempty"` AudioBlob []byte `json:"audio_blob"`
Duration *int32 `json:"duration,omitempty"` Duration *int32 `json:"duration"`
AudioType *string `json:"audio_type,omitempty"` AudioType *string `json:"audio_type"`
AudioUrl *string `json:"audio_url,omitempty"` AudioUrl *string `json:"audio_url"`
Frequency int `json:"frequency,omitempty"` Frequency int `json:"frequency"`
Frequencies []int `json:"frequencies,omitempty"` Frequencies []int `json:"frequencies"`
Patches []int `json:"patches,omitempty"` Patches []int `json:"patches"`
TGLabel *string `json:"tg_label,omitempty"` TGLabel *string `json:"tg_label"`
TGAlphaTag *string `json:"tg_alpha_tag,omitempty"` TGAlphaTag *string `json:"tg_alpha_tag"`
TGGroup *string `json:"tg_group,omitempty"` TGGroup *string `json:"tg_group"`
Source int `json:"source,omitempty"` Source int `json:"source"`
Transcript *string `json:"transcript,omitempty"` Transcript *string `json:"transcript"`
} }
type Incident struct { type Incident struct {
ID uuid.UUID `json:"id,omitempty"` ID uuid.UUID `json:"id"`
Name string `json:"name,omitempty"` Name string `json:"name"`
Owner int `json:"owner,omitempty"` Owner int `json:"owner"`
Description *string `json:"description,omitempty"` Description *string `json:"description"`
StartTime pgtype.Timestamptz `json:"start_time,omitempty"` StartTime pgtype.Timestamptz `json:"start_time"`
EndTime pgtype.Timestamptz `json:"end_time,omitempty"` EndTime pgtype.Timestamptz `json:"end_time"`
Location []byte `json:"location,omitempty"` Location []byte `json:"location"`
Metadata jsontypes.Metadata `json:"metadata,omitempty"` Metadata jsontypes.Metadata `json:"metadata"`
} }
type IncidentsCall struct { type IncidentsCall struct {
IncidentID uuid.UUID `json:"incident_id,omitempty"` IncidentID uuid.UUID `json:"incident_id"`
CallID uuid.UUID `json:"call_id,omitempty"` CallID uuid.UUID `json:"call_id"`
CallsTblID pgtype.UUID `json:"calls_tbl_id,omitempty"` CallsTblID pgtype.UUID `json:"calls_tbl_id"`
SweptCallID pgtype.UUID `json:"swept_call_id,omitempty"` SweptCallID pgtype.UUID `json:"swept_call_id"`
CallDate pgtype.Timestamptz `json:"call_date,omitempty"` CallDate pgtype.Timestamptz `json:"call_date"`
Notes []byte `json:"notes,omitempty"` Notes []byte `json:"notes"`
} }
type Setting struct { type Setting struct {
Name string `json:"name,omitempty"` Name string `json:"name"`
UpdatedBy *int32 `json:"updated_by,omitempty"` UpdatedBy *int32 `json:"updated_by"`
Value []byte `json:"value,omitempty"` Value []byte `json:"value"`
} }
type Share struct { type Share struct {
ID string `json:"id,omitempty"` ID string `json:"id"`
EntityType string `json:"entity_type,omitempty"` EntityType string `json:"entity_type"`
EntityID uuid.UUID `json:"entity_id,omitempty"` EntityID uuid.UUID `json:"entity_id"`
EntityDate pgtype.Timestamptz `json:"entity_date,omitempty"` EntityDate pgtype.Timestamptz `json:"entity_date"`
Owner int `json:"owner,omitempty"` Owner int `json:"owner"`
Expiration pgtype.Timestamptz `json:"expiration,omitempty"` Expiration pgtype.Timestamptz `json:"expiration"`
} }
type SweptCall struct { type SweptCall struct {
ID uuid.UUID `json:"id,omitempty"` ID uuid.UUID `json:"id"`
Submitter *int32 `json:"submitter,omitempty"` Submitter *int32 `json:"submitter"`
System int `json:"system,omitempty"` System int `json:"system"`
Talkgroup int `json:"talkgroup,omitempty"` Talkgroup int `json:"talkgroup"`
CallDate pgtype.Timestamptz `json:"call_date,omitempty"` CallDate pgtype.Timestamptz `json:"call_date"`
AudioName *string `json:"audio_name,omitempty"` AudioName *string `json:"audio_name"`
AudioBlob []byte `json:"audio_blob,omitempty"` AudioBlob []byte `json:"audio_blob"`
Duration *int32 `json:"duration,omitempty"` Duration *int32 `json:"duration"`
AudioType *string `json:"audio_type,omitempty"` AudioType *string `json:"audio_type"`
AudioUrl *string `json:"audio_url,omitempty"` AudioUrl *string `json:"audio_url"`
Frequency int `json:"frequency,omitempty"` Frequency int `json:"frequency"`
Frequencies []int `json:"frequencies,omitempty"` Frequencies []int `json:"frequencies"`
Patches []int `json:"patches,omitempty"` Patches []int `json:"patches"`
TGLabel *string `json:"tg_label,omitempty"` TGLabel *string `json:"tg_label"`
TGAlphaTag *string `json:"tg_alpha_tag,omitempty"` TGAlphaTag *string `json:"tg_alpha_tag"`
TGGroup *string `json:"tg_group,omitempty"` TGGroup *string `json:"tg_group"`
Source int `json:"source,omitempty"` Source int `json:"source"`
Transcript *string `json:"transcript,omitempty"` Transcript *string `json:"transcript"`
} }
type System struct { type System struct {
ID int `json:"id,omitempty"` ID int `json:"id"`
Name string `json:"name,omitempty"` Name string `json:"name"`
} }
type Talkgroup struct { type Talkgroup struct {
ID int `json:"id,omitempty"` ID int `json:"id"`
SystemID int32 `json:"system_id,omitempty"` SystemID int32 `json:"system_id"`
TGID int32 `json:"tgid,omitempty"` TGID int32 `json:"tgid"`
Name *string `json:"name,omitempty"` Name *string `json:"name"`
AlphaTag *string `json:"alpha_tag,omitempty"` AlphaTag *string `json:"alpha_tag"`
TGGroup *string `json:"tg_group,omitempty"` TGGroup *string `json:"tg_group"`
Frequency *int32 `json:"frequency,omitempty"` Frequency *int32 `json:"frequency"`
Metadata jsontypes.Metadata `json:"metadata,omitempty"` Metadata jsontypes.Metadata `json:"metadata"`
Tags []string `json:"tags,omitempty"` Tags []string `json:"tags"`
Alert bool `json:"alert,omitempty"` Alert bool `json:"alert"`
AlertRules rules.AlertRules `json:"alert_rules,omitempty"` AlertRules rules.AlertRules `json:"alert_rules"`
Weight float32 `json:"weight,omitempty"` Weight float32 `json:"weight"`
Learned bool `json:"learned,omitempty"` Learned bool `json:"learned"`
Ignored bool `json:"ignored,omitempty"` Ignored bool `json:"ignored"`
} }
type TalkgroupVersion struct { type TalkgroupVersion struct {
ID int `json:"id,omitempty"` ID int `json:"id"`
Time pgtype.Timestamptz `json:"time,omitempty"` Time pgtype.Timestamptz `json:"time"`
CreatedBy *int32 `json:"created_by,omitempty"` CreatedBy *int32 `json:"created_by"`
Deleted *bool `json:"deleted,omitempty"` Deleted *bool `json:"deleted"`
SystemID *int32 `json:"system_id,omitempty"` SystemID *int32 `json:"system_id"`
TGID *int32 `json:"tgid,omitempty"` TGID *int32 `json:"tgid"`
Name *string `json:"name,omitempty"` Name *string `json:"name"`
AlphaTag *string `json:"alpha_tag,omitempty"` AlphaTag *string `json:"alpha_tag"`
TGGroup *string `json:"tg_group,omitempty"` TGGroup *string `json:"tg_group"`
Frequency *int32 `json:"frequency,omitempty"` Frequency *int32 `json:"frequency"`
Metadata []byte `json:"metadata,omitempty"` Metadata []byte `json:"metadata"`
Tags []string `json:"tags,omitempty"` Tags []string `json:"tags"`
Alert *bool `json:"alert,omitempty"` Alert *bool `json:"alert"`
AlertRules []byte `json:"alert_rules,omitempty"` AlertRules []byte `json:"alert_rules"`
Weight *float32 `json:"weight,omitempty"` Weight *float32 `json:"weight"`
Learned *bool `json:"learned,omitempty"` Learned *bool `json:"learned"`
Ignored *bool `json:"ignored,omitempty"` Ignored *bool `json:"ignored"`
} }
type User struct { type User struct {
ID int `json:"id,omitempty"` ID int `json:"id"`
Username string `json:"username,omitempty"` Username string `json:"username"`
Password string `json:"password,omitempty"` Password string `json:"password"`
Email string `json:"email,omitempty"` Email string `json:"email"`
IsAdmin bool `json:"is_admin,omitempty"` IsAdmin bool `json:"is_admin"`
Prefs []byte `json:"prefs,omitempty"` Prefs []byte `json:"prefs"`
} }

View file

@ -16,6 +16,7 @@ type Querier interface {
AddCall(ctx context.Context, arg AddCallParams) error AddCall(ctx context.Context, arg AddCallParams) error
AddLearnedTalkgroup(ctx context.Context, arg AddLearnedTalkgroupParams) (Talkgroup, error) AddLearnedTalkgroup(ctx context.Context, arg AddLearnedTalkgroupParams) (Talkgroup, error)
AddToIncident(ctx context.Context, incidentID uuid.UUID, callIds []uuid.UUID, notes [][]byte) error AddToIncident(ctx context.Context, incidentID uuid.UUID, callIds []uuid.UUID, notes [][]byte) error
CallInIncident(ctx context.Context, incidentID uuid.UUID, callID uuid.UUID) (bool, error)
CleanupSweptCalls(ctx context.Context, rangeStart pgtype.Timestamptz, rangeEnd pgtype.Timestamptz) (int64, error) CleanupSweptCalls(ctx context.Context, rangeStart pgtype.Timestamptz, rangeEnd pgtype.Timestamptz) (int64, error)
CreateAPIKey(ctx context.Context, owner int, expires pgtype.Timestamp, disabled *bool) (ApiKey, error) CreateAPIKey(ctx context.Context, owner int, expires pgtype.Timestamp, disabled *bool) (ApiKey, error)
CreateIncident(ctx context.Context, arg CreateIncidentParams) (Incident, error) CreateIncident(ctx context.Context, arg CreateIncidentParams) (Incident, error)

View file

@ -5,7 +5,6 @@ import (
"dynatron.me/x/stillbox/internal/jsontypes" "dynatron.me/x/stillbox/internal/jsontypes"
"dynatron.me/x/stillbox/pkg/calls" "dynatron.me/x/stillbox/pkg/calls"
"dynatron.me/x/stillbox/pkg/rbac"
"dynatron.me/x/stillbox/pkg/users" "dynatron.me/x/stillbox/pkg/users"
"github.com/google/uuid" "github.com/google/uuid"
) )

View file

@ -0,0 +1,65 @@
package incstore
import (
"context"
"errors"
"fmt"
"github.com/el-mike/restrict/v2"
"github.com/google/uuid"
)
const (
CallInIncidentConditionType = "CALL_IN_INCIDENT"
)
type CallInIncidentCondition struct {
ID string `json:"name,omitempty" yaml:"name,omitempty"`
Call *restrict.ValueDescriptor `json:"call" yaml:"call"`
Incident *restrict.ValueDescriptor `json:"incident" yaml:"incident"`
}
func (*CallInIncidentCondition) Type() string {
return CallInIncidentConditionType
}
func (c *CallInIncidentCondition) Check(r *restrict.AccessRequest) error {
callVID, err := c.Call.GetValue(r)
if err != nil {
return err
}
incVID, err := c.Incident.GetValue(r)
if err != nil {
return err
}
ctx, hasCtx := r.Context["ctx"].(context.Context)
if !hasCtx {
return restrict.NewConditionNotSatisfiedError(c, r, fmt.Errorf("no context provided"))
}
incID, isUUID := incVID.(uuid.UUID)
if !isUUID {
return restrict.NewConditionNotSatisfiedError(c, r, errors.New("incident ID is not UUID"))
}
callID, isUUID := callVID.(uuid.UUID)
if !isUUID {
return restrict.NewConditionNotSatisfiedError(c, r, errors.New("call ID is not UUID"))
}
incs := FromCtx(ctx)
inCall, err := incs.CallIn(ctx, incID, incID)
if err != nil {
return restrict.NewConditionNotSatisfiedError(c, r, err)
}
if !inCall {
return restrict.NewConditionNotSatisfiedError(c, r, fmt.Errorf(`incident "%v" not in call "%v"`, incID, callID))
}
return nil
}

View file

@ -49,6 +49,9 @@ type Store interface {
// Owner returns an incident with only the owner filled out. // Owner returns an incident with only the owner filled out.
Owner(ctx context.Context, id uuid.UUID) (incidents.Incident, error) Owner(ctx context.Context, id uuid.UUID) (incidents.Incident, error)
// CallIn returns whether an incident is in an call
CallIn(ctx context.Context, inc uuid.UUID, call uuid.UUID) (bool, error)
} }
type store struct { type store struct {
@ -373,3 +376,9 @@ func (s *store) Owner(ctx context.Context, id uuid.UUID) (incidents.Incident, er
owner, err := database.FromCtx(ctx).GetIncidentOwner(ctx, id) owner, err := database.FromCtx(ctx).GetIncidentOwner(ctx, id)
return incidents.Incident{ID: id, Owner: users.UserID(owner)}, err return incidents.Incident{ID: id, Owner: users.UserID(owner)}, err
} }
func (s *store) CallIn(ctx context.Context, inc uuid.UUID, call uuid.UUID) (bool, error) {
db := database.FromCtx(ctx)
return db.CallInIncident(ctx, inc, call)
}

View file

@ -77,12 +77,9 @@ func (c *InMapCondition[K, V]) Check(r *restrict.AccessRequest) error {
return err return err
} }
keyVal := reflect.ValueOf(cKey) key := cKey.(K)
mapVal := reflect.ValueOf(cMap)
key := keyVal.Interface().(K) if _, in := cMap.(map[K]V)[key]; !in {
if _, in := mapVal.Interface().(map[K]V)[key]; !in {
return restrict.NewConditionNotSatisfiedError(c, r, fmt.Errorf("key '%v' not in map", key)) return restrict.NewConditionNotSatisfiedError(c, r, fmt.Errorf("key '%v' not in map", key))
} }

23
pkg/rbac/entities.go Normal file
View file

@ -0,0 +1,23 @@
package rbac
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"
ActionShare = "share"
)

View file

@ -1,10 +1,25 @@
package rbac package rbac
import ( import (
"dynatron.me/x/stillbox/pkg/incidents/incstore"
"github.com/el-mike/restrict/v2" "github.com/el-mike/restrict/v2"
) )
var policy = &restrict.PolicyDefinition{ const (
PresetUpdateOwn = "updateOwn"
PresetDeleteOwn = "deleteOwn"
PresetReadShared = "readShared"
PresetReadSharedInMap = "readSharedInMap"
PresetShareOwn = "shareOwn"
PresetUpdateSubmitter = "updateSubmitter"
PresetDeleteSubmitter = "deleteSubmitter"
PresetShareSubmitter = "shareSubmitter"
PresetReadInSharedIncident = "readInSharedIncident"
)
var Policy = &restrict.PolicyDefinition{
Roles: restrict.Roles{ Roles: restrict.Roles{
RoleUser: { RoleUser: {
Description: "An authenticated user", Description: "An authenticated user",
@ -52,6 +67,7 @@ var policy = &restrict.PolicyDefinition{
Grants: restrict.GrantsMap{ Grants: restrict.GrantsMap{
ResourceCall: { ResourceCall: {
&restrict.Permission{Preset: PresetReadShared}, &restrict.Permission{Preset: PresetReadShared},
&restrict.Permission{Preset: PresetReadInSharedIncident},
}, },
ResourceIncident: { ResourceIncident: {
&restrict.Permission{Preset: PresetReadShared}, &restrict.Permission{Preset: PresetReadShared},
@ -207,5 +223,21 @@ var policy = &restrict.PolicyDefinition{
}, },
}, },
}, },
PresetReadInSharedIncident: &restrict.Permission{
Action: ActionRead,
Conditions: restrict.Conditions{
&incstore.CallInIncidentCondition{
ID: "callInIncident",
Call: &restrict.ValueDescriptor{
Source: restrict.ResourceField,
Field: "ID",
},
Incident: &restrict.ValueDescriptor{
Source: restrict.SubjectField,
Field: "EntityID",
},
},
},
},
}, },
} }

View file

@ -8,37 +8,7 @@ import (
"github.com/el-mike/restrict/v2/adapters" "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"
ActionShare = "share"
PresetUpdateOwn = "updateOwn"
PresetDeleteOwn = "deleteOwn"
PresetReadShared = "readShared"
PresetReadSharedInMap = "readSharedInMap"
PresetShareOwn = "shareOwn"
PresetUpdateSubmitter = "updateSubmitter"
PresetDeleteSubmitter = "deleteSubmitter"
PresetShareSubmitter = "shareSubmitter"
)
var ( var (
ErrBadSubject = errors.New("bad subject in token") ErrBadSubject = errors.New("bad subject in token")
@ -132,7 +102,7 @@ type rbac struct {
} }
func New() (*rbac, error) { func New() (*rbac, error) {
adapter := adapters.NewInMemoryAdapter(policy) adapter := adapters.NewInMemoryAdapter(Policy)
polMan, err := restrict.NewPolicyManager(adapter, true) polMan, err := restrict.NewPolicyManager(adapter, true)
if err != nil { if err != nil {
return nil, err return nil, err
@ -154,10 +124,17 @@ func (r *rbac) Check(ctx context.Context, res restrict.Resource, opts ...CheckOp
sub := SubjectFrom(ctx) sub := SubjectFrom(ctx)
o := checkOptions{} o := checkOptions{}
for _, opt := range opts { for _, opt := range opts {
opt(&o) opt(&o)
} }
if o.context == nil {
o.context = make(restrict.Context)
}
o.context["ctx"] = ctx
req := &restrict.AccessRequest{ req := &restrict.AccessRequest{
Subject: sub, Subject: sub,
Resource: res, Resource: res,

View file

@ -2,7 +2,6 @@ package server
import ( import (
"errors" "errors"
"fmt"
"io/fs" "io/fs"
"net/http" "net/http"
"path" "path"

View file

@ -9,7 +9,10 @@ import (
"strings" "strings"
"dynatron.me/x/stillbox/pkg/database" "dynatron.me/x/stillbox/pkg/database"
"dynatron.me/x/stillbox/pkg/rbac" )
const (
Resource = "Talkgroup"
) )
type Talkgroup struct { type Talkgroup struct {
@ -19,7 +22,7 @@ type Talkgroup struct {
} }
func (t *Talkgroup) GetResourceName() string { func (t *Talkgroup) GetResourceName() string {
return rbac.ResourceTalkgroup return Resource
} }
func (t Talkgroup) String() string { func (t Talkgroup) String() string {

View file

@ -327,7 +327,7 @@ func addToRowList[T rowType](t *cache, tgRecords []T) []*tgsp.Talkgroup {
} }
func (t *cache) TGs(ctx context.Context, tgs tgsp.IDs, opts ...Option) ([]*tgsp.Talkgroup, error) { func (t *cache) TGs(ctx context.Context, tgs tgsp.IDs, opts ...Option) ([]*tgsp.Talkgroup, error) {
_, err := rbac.Check(ctx, rbac.UseResource(rbac.ResourceTalkgroup), rbac.WithActions(rbac.ActionRead)) _, err := rbac.Check(ctx, rbac.UseResource(tgsp.Resource), rbac.WithActions(rbac.ActionRead))
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -1,13 +0,0 @@
package users
import (
"dynatron.me/x/stillbox/pkg/rbac"
)
type Public struct {
RemoteAddr string
}
func (s *Public) GetRoles() []string {
return []string{rbac.RolePublic}
}

View file

@ -29,6 +29,13 @@ UPDATE incidents_Calls
SET notes = @notes SET notes = @notes
WHERE incident_id = @incident_id AND call_id = @call_id; WHERE incident_id = @incident_id AND call_id = @call_id;
-- name: CallInIncident :one
SELECT EXISTS
(SELECT 1 FROM incidents_calls ic
WHERE
ic.incident_id = @incident_id AND
ic.call_id = @call_id);
-- name: CreateIncident :one -- name: CreateIncident :one
INSERT INTO incidents ( INSERT INTO incidents (
id, id,