Able to use store in rbac policy finally

This commit is contained in:
Daniel Ponte 2025-01-22 10:39:23 -05:00
parent 957aebe695
commit 76a2214377
31 changed files with 510 additions and 332 deletions

View file

@ -14,7 +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/rbac/entities"
"dynatron.me/x/stillbox/pkg/sinks"
"dynatron.me/x/stillbox/pkg/talkgroups"
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
@ -124,7 +124,7 @@ 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"})
ctx = entities.CtxWithSubject(ctx, &entities.SystemServiceSubject{Name: "alerter"})
err := as.startBackfill(ctx)
if err != nil {

View file

@ -7,7 +7,7 @@ import (
"time"
"dynatron.me/x/stillbox/pkg/database"
"dynatron.me/x/stillbox/pkg/rbac"
"dynatron.me/x/stillbox/pkg/rbac/entities"
"github.com/google/uuid"
"github.com/rs/zerolog/log"
@ -16,10 +16,10 @@ import (
type apiKeyAuth interface {
// 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) (rbac.Subject, error)
CheckAPIKey(ctx context.Context, key string) (entities.Subject, error)
}
func (a *Auth) CheckAPIKey(ctx context.Context, key string) (rbac.Subject, error) {
func (a *Auth) CheckAPIKey(ctx context.Context, key string) (entities.Subject, error) {
keyUuid, err := uuid.Parse(key)
if err != nil {
log.Error().Str("apikey", key).Msg("cannot parse key")

View file

@ -10,7 +10,7 @@ import (
"golang.org/x/crypto/bcrypt"
"dynatron.me/x/stillbox/pkg/database"
"dynatron.me/x/stillbox/pkg/rbac"
"dynatron.me/x/stillbox/pkg/rbac/entities"
"dynatron.me/x/stillbox/pkg/users"
"github.com/go-chi/chi/v5"
@ -104,7 +104,7 @@ func (a *Auth) AuthMiddleware() func(http.Handler) http.Handler {
return
}
ctx = rbac.CtxWithSubject(ctx, sub)
ctx = entities.CtxWithSubject(ctx, sub)
next.ServeHTTP(w, r.WithContext(ctx))

View file

@ -8,7 +8,7 @@ import (
"dynatron.me/x/stillbox/internal/audio"
"dynatron.me/x/stillbox/internal/jsontypes"
"dynatron.me/x/stillbox/pkg/pb"
"dynatron.me/x/stillbox/pkg/rbac"
"dynatron.me/x/stillbox/pkg/rbac/entities"
"dynatron.me/x/stillbox/pkg/talkgroups"
"dynatron.me/x/stillbox/pkg/users"
@ -76,7 +76,7 @@ type Call struct {
}
func (c *Call) GetResourceName() string {
return rbac.ResourceCall
return entities.ResourceCall
}
func (c *Call) String() string {

View file

@ -11,6 +11,7 @@ import (
"dynatron.me/x/stillbox/pkg/calls"
"dynatron.me/x/stillbox/pkg/database"
"dynatron.me/x/stillbox/pkg/rbac"
"dynatron.me/x/stillbox/pkg/rbac/entities"
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
"dynatron.me/x/stillbox/pkg/users"
@ -85,7 +86,7 @@ func toAddCallParams(call *calls.Call) database.AddCallParams {
}
func (s *store) AddCall(ctx context.Context, call *calls.Call) error {
_, err := rbac.Check(ctx, call, rbac.WithActions(rbac.ActionCreate))
_, err := rbac.Check(ctx, call, rbac.WithActions(entities.ActionCreate))
if err != nil {
return err
}
@ -123,7 +124,7 @@ func (s *store) AddCall(ctx context.Context, call *calls.Call) error {
}
func (s *store) CallAudio(ctx context.Context, id uuid.UUID) (*calls.CallAudio, error) {
_, err := rbac.Check(ctx, &calls.Call{ID: id}, rbac.WithActions(rbac.ActionRead))
_, err := rbac.Check(ctx, &calls.Call{ID: id}, rbac.WithActions(entities.ActionRead))
if err != nil {
return nil, err
}
@ -144,7 +145,7 @@ func (s *store) CallAudio(ctx context.Context, id uuid.UUID) (*calls.CallAudio,
}
func (s *store) Call(ctx context.Context, id uuid.UUID) (*calls.Call, error) {
_, err := rbac.Check(ctx, rbac.UseResource(rbac.ResourceCall), rbac.WithActions(rbac.ActionRead))
_, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceCall), rbac.WithActions(entities.ActionRead))
if err != nil {
return nil, err
}
@ -193,7 +194,7 @@ 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))
_, err = rbac.Check(ctx, rbac.UseResource(entities.ResourceCall), rbac.WithActions(entities.ActionRead))
if err != nil {
return nil, 0, err
}
@ -256,7 +257,7 @@ func (s *store) Delete(ctx context.Context, id uuid.UUID) error {
return err
}
_, err = rbac.Check(ctx, &callOwn, rbac.WithActions(rbac.ActionDelete))
_, err = rbac.Check(ctx, &callOwn, rbac.WithActions(entities.ActionDelete))
if err != nil {
return err
}

View file

@ -278,6 +278,64 @@ func (_c *Store_BulkSetTalkgroupTags_Call) RunAndReturn(run func(context.Context
return _c
}
// CallInIncident provides a mock function with given fields: ctx, incidentID, callID
func (_m *Store) CallInIncident(ctx context.Context, incidentID uuid.UUID, callID uuid.UUID) (bool, error) {
ret := _m.Called(ctx, incidentID, callID)
if len(ret) == 0 {
panic("no return value specified for CallInIncident")
}
var r0 bool
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID, uuid.UUID) (bool, error)); ok {
return rf(ctx, incidentID, callID)
}
if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID, uuid.UUID) bool); ok {
r0 = rf(ctx, incidentID, callID)
} else {
r0 = ret.Get(0).(bool)
}
if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID, uuid.UUID) error); ok {
r1 = rf(ctx, incidentID, callID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Store_CallInIncident_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CallInIncident'
type Store_CallInIncident_Call struct {
*mock.Call
}
// CallInIncident is a helper method to define mock.On call
// - ctx context.Context
// - incidentID uuid.UUID
// - callID uuid.UUID
func (_e *Store_Expecter) CallInIncident(ctx interface{}, incidentID interface{}, callID interface{}) *Store_CallInIncident_Call {
return &Store_CallInIncident_Call{Call: _e.mock.On("CallInIncident", ctx, incidentID, callID)}
}
func (_c *Store_CallInIncident_Call) Run(run func(ctx context.Context, incidentID uuid.UUID, callID uuid.UUID)) *Store_CallInIncident_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(uuid.UUID), args[2].(uuid.UUID))
})
return _c
}
func (_c *Store_CallInIncident_Call) Return(_a0 bool, _a1 error) *Store_CallInIncident_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *Store_CallInIncident_Call) RunAndReturn(run func(context.Context, uuid.UUID, uuid.UUID) (bool, error)) *Store_CallInIncident_Call {
_c.Call.Return(run)
return _c
}
// CleanupSweptCalls provides a mock function with given fields: ctx, rangeStart, rangeEnd
func (_m *Store) CleanupSweptCalls(ctx context.Context, rangeStart pgtype.Timestamptz, rangeEnd pgtype.Timestamptz) (int64, error) {
ret := _m.Called(ctx, rangeStart, rangeEnd)
@ -1750,6 +1808,65 @@ func (_c *Store_GetIncidentOwner_Call) RunAndReturn(run func(context.Context, uu
return _c
}
// GetIncidentTalkgroups provides a mock function with given fields: ctx, incidentID
func (_m *Store) GetIncidentTalkgroups(ctx context.Context, incidentID uuid.UUID) ([]database.GetIncidentTalkgroupsRow, error) {
ret := _m.Called(ctx, incidentID)
if len(ret) == 0 {
panic("no return value specified for GetIncidentTalkgroups")
}
var r0 []database.GetIncidentTalkgroupsRow
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) ([]database.GetIncidentTalkgroupsRow, error)); ok {
return rf(ctx, incidentID)
}
if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) []database.GetIncidentTalkgroupsRow); ok {
r0 = rf(ctx, incidentID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]database.GetIncidentTalkgroupsRow)
}
}
if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID) error); ok {
r1 = rf(ctx, incidentID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Store_GetIncidentTalkgroups_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetIncidentTalkgroups'
type Store_GetIncidentTalkgroups_Call struct {
*mock.Call
}
// GetIncidentTalkgroups is a helper method to define mock.On call
// - ctx context.Context
// - incidentID uuid.UUID
func (_e *Store_Expecter) GetIncidentTalkgroups(ctx interface{}, incidentID interface{}) *Store_GetIncidentTalkgroups_Call {
return &Store_GetIncidentTalkgroups_Call{Call: _e.mock.On("GetIncidentTalkgroups", ctx, incidentID)}
}
func (_c *Store_GetIncidentTalkgroups_Call) Run(run func(ctx context.Context, incidentID uuid.UUID)) *Store_GetIncidentTalkgroups_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(uuid.UUID))
})
return _c
}
func (_c *Store_GetIncidentTalkgroups_Call) Return(_a0 []database.GetIncidentTalkgroupsRow, _a1 error) *Store_GetIncidentTalkgroups_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *Store_GetIncidentTalkgroups_Call) RunAndReturn(run func(context.Context, uuid.UUID) ([]database.GetIncidentTalkgroupsRow, error)) *Store_GetIncidentTalkgroups_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)

View file

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

View file

@ -13,7 +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"
"dynatron.me/x/stillbox/pkg/rbac/entities"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
@ -135,7 +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"})
ctx = entities.CtxWithSubject(ctx, &entities.SystemServiceSubject{Name: "partman"})
tick := time.NewTicker(CheckInterval)
select {

View file

@ -40,6 +40,7 @@ type Querier interface {
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)
GetIncidentTalkgroups(ctx context.Context, incidentID uuid.UUID) ([]GetIncidentTalkgroupsRow, 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)

View file

@ -53,6 +53,40 @@ func (q *Queries) DeleteShare(ctx context.Context, id string) error {
return err
}
const getIncidentTalkgroups = `-- name: GetIncidentTalkgroups :many
SELECT DISTINCT
c.system,
c.talkgroup
FROM incidents_calls ic
JOIN calls c ON (c.id = ic.call_id AND c.call_date = ic.call_date)
WHERE ic.incident_id = $1
`
type GetIncidentTalkgroupsRow struct {
System int `json:"system"`
Talkgroup int `json:"talkgroup"`
}
func (q *Queries) GetIncidentTalkgroups(ctx context.Context, incidentID uuid.UUID) ([]GetIncidentTalkgroupsRow, error) {
rows, err := q.db.Query(ctx, getIncidentTalkgroups, incidentID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetIncidentTalkgroupsRow
for rows.Next() {
var i GetIncidentTalkgroupsRow
if err := rows.Scan(&i.System, &i.Talkgroup); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getShare = `-- name: GetShare :one
SELECT
id,

View file

@ -5,7 +5,7 @@ import (
"dynatron.me/x/stillbox/internal/jsontypes"
"dynatron.me/x/stillbox/pkg/calls"
"dynatron.me/x/stillbox/pkg/rbac"
"dynatron.me/x/stillbox/pkg/rbac/entities"
"dynatron.me/x/stillbox/pkg/users"
"github.com/google/uuid"
)
@ -23,7 +23,7 @@ type Incident struct {
}
func (inc *Incident) GetResourceName() string {
return rbac.ResourceIncident
return entities.ResourceIncident
}
type IncidentCall struct {

View file

@ -10,6 +10,7 @@ import (
"dynatron.me/x/stillbox/pkg/database"
"dynatron.me/x/stillbox/pkg/incidents"
"dynatron.me/x/stillbox/pkg/rbac"
"dynatron.me/x/stillbox/pkg/rbac/entities"
"dynatron.me/x/stillbox/pkg/users"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
@ -143,7 +144,7 @@ func (s *store) AddRemoveIncidentCalls(ctx context.Context, incidentID uuid.UUID
return err
}
_, err = rbac.Check(ctx, &inc, rbac.WithActions(rbac.ActionUpdate))
_, err = rbac.Check(ctx, &inc, rbac.WithActions(entities.ActionUpdate))
if err != nil {
return err
}
@ -176,7 +177,7 @@ 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))
_, err = rbac.Check(ctx, new(incidents.Incident), rbac.WithActions(entities.ActionRead))
if err != nil {
return nil, 0, err
}
@ -281,7 +282,7 @@ func fromDBCalls(d []database.GetIncidentCallsRow) []incidents.IncidentCall {
}
func (s *store) Incident(ctx context.Context, id uuid.UUID) (*incidents.Incident, error) {
_, err := rbac.Check(ctx, &incidents.Incident{ID: id}, rbac.WithActions(rbac.ActionRead))
_, err := rbac.Check(ctx, &incidents.Incident{ID: id}, rbac.WithActions(entities.ActionRead))
if err != nil {
return nil, err
}
@ -337,7 +338,7 @@ func (s *store) UpdateIncident(ctx context.Context, id uuid.UUID, p UpdateIncide
return nil, err
}
_, err = rbac.Check(ctx, &ckinc, rbac.WithActions(rbac.ActionUpdate))
_, err = rbac.Check(ctx, &ckinc, rbac.WithActions(entities.ActionUpdate))
if err != nil {
return nil, err
}
@ -360,7 +361,7 @@ func (s *store) DeleteIncident(ctx context.Context, id uuid.UUID) error {
return err
}
_, err = rbac.Check(ctx, &inc, rbac.WithActions(rbac.ActionDelete))
_, err = rbac.Check(ctx, &inc, rbac.WithActions(entities.ActionDelete))
if err != nil {
return err
}

View file

@ -6,7 +6,7 @@ import (
"dynatron.me/x/stillbox/pkg/calls"
"dynatron.me/x/stillbox/pkg/pb"
"dynatron.me/x/stillbox/pkg/rbac"
"dynatron.me/x/stillbox/pkg/rbac/entities"
"github.com/rs/zerolog/log"
)
@ -39,7 +39,7 @@ func New() *Nexus {
}
func (n *Nexus) Go(ctx context.Context) {
ctx = rbac.CtxWithSubject(ctx, &rbac.SystemServiceSubject{Name: "nexus"})
ctx = entities.CtxWithSubject(ctx, &entities.SystemServiceSubject{Name: "nexus"})
for {
select {
case call, ok := <-n.callCh:

View file

@ -1,23 +0,0 @@
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

@ -0,0 +1,79 @@
package entities
import (
"context"
"github.com/el-mike/restrict/v2"
)
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"
)
func SubjectFrom(ctx context.Context) Subject {
sub, ok := ctx.Value(SubjectCtxKey).(Subject)
if ok {
return sub
}
return new(PublicSubject)
}
type Subject interface {
restrict.Subject
GetName() string
}
func CtxWithSubject(ctx context.Context, sub Subject) context.Context {
return context.WithValue(ctx, SubjectCtxKey, sub)
}
type subjectContextKey string
const SubjectCtxKey subjectContextKey = "sub"
type Resource interface {
restrict.Resource
}
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}
}

View file

@ -5,9 +5,11 @@ package mocks
import (
context "context"
rbac "dynatron.me/x/stillbox/pkg/rbac"
entities "dynatron.me/x/stillbox/pkg/rbac/entities"
mock "github.com/stretchr/testify/mock"
rbac "dynatron.me/x/stillbox/pkg/rbac"
restrict "github.com/el-mike/restrict/v2"
)
@ -25,7 +27,7 @@ func (_m *RBAC) EXPECT() *RBAC_Expecter {
}
// 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) {
func (_m *RBAC) Check(ctx context.Context, res restrict.Resource, opts ...rbac.CheckOption) (entities.Subject, error) {
_va := make([]interface{}, len(opts))
for _i := range opts {
_va[_i] = opts[_i]
@ -39,16 +41,16 @@ func (_m *RBAC) Check(ctx context.Context, res restrict.Resource, opts ...rbac.C
panic("no return value specified for Check")
}
var r0 rbac.Subject
var r0 entities.Subject
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, restrict.Resource, ...rbac.CheckOption) (rbac.Subject, error)); ok {
if rf, ok := ret.Get(0).(func(context.Context, restrict.Resource, ...rbac.CheckOption) (entities.Subject, error)); ok {
return rf(ctx, res, opts...)
}
if rf, ok := ret.Get(0).(func(context.Context, restrict.Resource, ...rbac.CheckOption) rbac.Subject); ok {
if rf, ok := ret.Get(0).(func(context.Context, restrict.Resource, ...rbac.CheckOption) entities.Subject); ok {
r0 = rf(ctx, res, opts...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(rbac.Subject)
r0 = ret.Get(0).(entities.Subject)
}
}
@ -88,12 +90,12 @@ func (_c *RBAC_Check_Call) Run(run func(ctx context.Context, res restrict.Resour
return _c
}
func (_c *RBAC_Check_Call) Return(_a0 rbac.Subject, _a1 error) *RBAC_Check_Call {
func (_c *RBAC_Check_Call) Return(_a0 entities.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 {
func (_c *RBAC_Check_Call) RunAndReturn(run func(context.Context, restrict.Resource, ...rbac.CheckOption) (entities.Subject, error)) *RBAC_Check_Call {
_c.Call.Return(run)
return _c
}

View file

@ -1,4 +1,4 @@
package rbac
package policy
import (
"context"
@ -6,7 +6,7 @@ import (
"fmt"
"reflect"
"dynatron.me/x/stillbox/pkg/database"
"dynatron.me/x/stillbox/pkg/incidents/incstore"
"github.com/el-mike/restrict/v2"
"github.com/google/uuid"
@ -54,7 +54,7 @@ func (c *CallInIncidentCondition) Check(r *restrict.AccessRequest) error {
return restrict.NewConditionNotSatisfiedError(c, r, errors.New("call ID is not UUID"))
}
inCall, err := database.FromCtx(ctx).CallInIncident(ctx, incID, callID)
inCall, err := incstore.FromCtx(ctx).CallIn(ctx, incID, callID)
if err != nil {
return restrict.NewConditionNotSatisfiedError(c, r, err)
}

View file

@ -1,6 +1,8 @@
package rbac
package policy
import (
"dynatron.me/x/stillbox/pkg/rbac/entities"
"github.com/el-mike/restrict/v2"
)
@ -19,90 +21,90 @@ const (
var Policy = &restrict.PolicyDefinition{
Roles: restrict.Roles{
RoleUser: {
entities.RoleUser: {
Description: "An authenticated user",
Grants: restrict.GrantsMap{
ResourceIncident: {
&restrict.Permission{Action: ActionRead},
&restrict.Permission{Action: ActionCreate},
entities.ResourceIncident: {
&restrict.Permission{Action: entities.ActionRead},
&restrict.Permission{Action: entities.ActionCreate},
&restrict.Permission{Preset: PresetUpdateOwn},
&restrict.Permission{Preset: PresetDeleteOwn},
&restrict.Permission{Preset: PresetShareOwn},
},
ResourceCall: {
&restrict.Permission{Action: ActionRead},
&restrict.Permission{Action: ActionCreate},
entities.ResourceCall: {
&restrict.Permission{Action: entities.ActionRead},
&restrict.Permission{Action: entities.ActionCreate},
&restrict.Permission{Preset: PresetUpdateSubmitter},
&restrict.Permission{Preset: PresetDeleteSubmitter},
&restrict.Permission{Action: ActionShare},
&restrict.Permission{Action: entities.ActionShare},
},
ResourceTalkgroup: {
&restrict.Permission{Action: ActionRead},
entities.ResourceTalkgroup: {
&restrict.Permission{Action: entities.ActionRead},
},
ResourceShare: {
&restrict.Permission{Action: ActionRead},
&restrict.Permission{Action: ActionCreate},
entities.ResourceShare: {
&restrict.Permission{Action: entities.ActionRead},
&restrict.Permission{Action: entities.ActionCreate},
&restrict.Permission{Preset: PresetUpdateOwn},
&restrict.Permission{Preset: PresetDeleteOwn},
},
},
},
RoleSubmitter: {
entities.RoleSubmitter: {
Description: "A role that can submit calls",
Grants: restrict.GrantsMap{
ResourceCall: {
&restrict.Permission{Action: ActionCreate},
entities.ResourceCall: {
&restrict.Permission{Action: entities.ActionCreate},
},
ResourceTalkgroup: {
entities.ResourceTalkgroup: {
// for learning TGs
&restrict.Permission{Action: ActionCreate},
&restrict.Permission{Action: ActionUpdate},
&restrict.Permission{Action: entities.ActionCreate},
&restrict.Permission{Action: entities.ActionUpdate},
},
},
},
RoleShareGuest: {
entities.RoleShareGuest: {
Description: "Someone who has a valid share link",
Grants: restrict.GrantsMap{
ResourceCall: {
entities.ResourceCall: {
&restrict.Permission{Preset: PresetReadShared},
&restrict.Permission{Preset: PresetReadInSharedIncident},
},
ResourceIncident: {
entities.ResourceIncident: {
&restrict.Permission{Preset: PresetReadShared},
},
ResourceTalkgroup: {
&restrict.Permission{Action: ActionRead},
entities.ResourceTalkgroup: {
&restrict.Permission{Action: entities.ActionRead},
},
},
},
RoleAdmin: {
Parents: []string{RoleUser},
entities.RoleAdmin: {
Parents: []string{entities.RoleUser},
Grants: restrict.GrantsMap{
ResourceIncident: {
&restrict.Permission{Action: ActionUpdate},
&restrict.Permission{Action: ActionDelete},
&restrict.Permission{Action: ActionShare},
entities.ResourceIncident: {
&restrict.Permission{Action: entities.ActionUpdate},
&restrict.Permission{Action: entities.ActionDelete},
&restrict.Permission{Action: entities.ActionShare},
},
ResourceCall: {
&restrict.Permission{Action: ActionUpdate},
&restrict.Permission{Action: ActionDelete},
&restrict.Permission{Action: ActionShare},
entities.ResourceCall: {
&restrict.Permission{Action: entities.ActionUpdate},
&restrict.Permission{Action: entities.ActionDelete},
&restrict.Permission{Action: entities.ActionShare},
},
ResourceTalkgroup: {
&restrict.Permission{Action: ActionUpdate},
&restrict.Permission{Action: ActionCreate},
&restrict.Permission{Action: ActionDelete},
entities.ResourceTalkgroup: {
&restrict.Permission{Action: entities.ActionUpdate},
&restrict.Permission{Action: entities.ActionCreate},
&restrict.Permission{Action: entities.ActionDelete},
},
},
},
RoleSystem: {
Parents: []string{RoleSystem},
entities.RoleSystem: {
Parents: []string{entities.RoleSystem},
},
RolePublic: {
entities.RolePublic: {
/*
Grants: restrict.GrantsMap{
ResourceShare: {
&restrict.Permission{Action: ActionRead},
entities.ResourceShare: {
&restrict.Permission{Action: entities.ActionRead},
},
},
*/
@ -110,7 +112,7 @@ var Policy = &restrict.PolicyDefinition{
},
PermissionPresets: restrict.PermissionPresets{
PresetUpdateOwn: &restrict.Permission{
Action: ActionUpdate,
Action: entities.ActionUpdate,
Conditions: restrict.Conditions{
&restrict.EqualCondition{
ID: "isOwner",
@ -126,7 +128,7 @@ var Policy = &restrict.PolicyDefinition{
},
},
PresetDeleteOwn: &restrict.Permission{
Action: ActionDelete,
Action: entities.ActionDelete,
Conditions: restrict.Conditions{
&restrict.EqualCondition{
ID: "isOwner",
@ -142,7 +144,7 @@ var Policy = &restrict.PolicyDefinition{
},
},
PresetShareOwn: &restrict.Permission{
Action: ActionShare,
Action: entities.ActionShare,
Conditions: restrict.Conditions{
&restrict.EqualCondition{
ID: "isOwner",
@ -158,7 +160,7 @@ var Policy = &restrict.PolicyDefinition{
},
},
PresetUpdateSubmitter: &restrict.Permission{
Action: ActionUpdate,
Action: entities.ActionUpdate,
Conditions: restrict.Conditions{
&SubmitterEqualCondition{
ID: "isSubmitter",
@ -174,7 +176,7 @@ var Policy = &restrict.PolicyDefinition{
},
},
PresetDeleteSubmitter: &restrict.Permission{
Action: ActionDelete,
Action: entities.ActionDelete,
Conditions: restrict.Conditions{
&SubmitterEqualCondition{
ID: "isSubmitter",
@ -190,7 +192,7 @@ var Policy = &restrict.PolicyDefinition{
},
},
PresetShareSubmitter: &restrict.Permission{
Action: ActionShare,
Action: entities.ActionShare,
Conditions: restrict.Conditions{
&SubmitterEqualCondition{
ID: "isSubmitter",
@ -206,7 +208,7 @@ var Policy = &restrict.PolicyDefinition{
},
},
PresetReadShared: &restrict.Permission{
Action: ActionRead,
Action: entities.ActionRead,
Conditions: restrict.Conditions{
&restrict.EqualCondition{
ID: "isOwner",
@ -222,7 +224,7 @@ var Policy = &restrict.PolicyDefinition{
},
},
PresetReadInSharedIncident: &restrict.Permission{
Action: ActionRead,
Action: entities.ActionRead,
Conditions: restrict.Conditions{
&CallInIncidentCondition{
ID: "callInIncident",

View file

@ -4,6 +4,8 @@ import (
"context"
"errors"
"dynatron.me/x/stillbox/pkg/rbac/entities"
"github.com/el-mike/restrict/v2"
"github.com/el-mike/restrict/v2/adapters"
)
@ -12,14 +14,6 @@ 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
@ -28,15 +22,6 @@ func ErrAccessDenied(err error) *restrict.AccessDeniedError {
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"
@ -81,17 +66,8 @@ 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)
Check(ctx context.Context, res restrict.Resource, opts ...CheckOption) (entities.Subject, error)
}
type rbac struct {
@ -99,8 +75,8 @@ type rbac struct {
access *restrict.AccessManager
}
func New() (*rbac, error) {
adapter := adapters.NewInMemoryAdapter(Policy)
func New(pol *restrict.PolicyDefinition) (*rbac, error) {
adapter := adapters.NewInMemoryAdapter(pol)
polMan, err := restrict.NewPolicyManager(adapter, true)
if err != nil {
return nil, err
@ -114,12 +90,12 @@ func New() (*rbac, error) {
}
// 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) {
func Check(ctx context.Context, res restrict.Resource, opts ...CheckOption) (entities.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)
func (r *rbac) Check(ctx context.Context, res restrict.Resource, opts ...CheckOption) (entities.Subject, error) {
sub := entities.SubjectFrom(ctx)
o := checkOptions{}
for _, opt := range opts {
@ -139,34 +115,5 @@ func (r *rbac) Check(ctx context.Context, res restrict.Resource, opts ...CheckOp
Context: o.context,
}
err := r.access.Authorize(req)
if err != nil {
return nil, err
}
return sub, nil
}
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}
return sub, r.access.Authorize(req)
}

View file

@ -10,6 +10,8 @@ import (
"dynatron.me/x/stillbox/pkg/calls"
"dynatron.me/x/stillbox/pkg/incidents"
"dynatron.me/x/stillbox/pkg/rbac"
"dynatron.me/x/stillbox/pkg/rbac/entities"
"dynatron.me/x/stillbox/pkg/rbac/policy"
"dynatron.me/x/stillbox/pkg/talkgroups"
"dynatron.me/x/stillbox/pkg/users"
"github.com/el-mike/restrict/v2"
@ -20,8 +22,8 @@ import (
func TestRBAC(t *testing.T) {
tests := []struct {
name string
subject rbac.Subject
resource rbac.Resource
subject entities.Subject
resource entities.Resource
action string
expectErr error
}{
@ -32,7 +34,7 @@ func TestRBAC(t *testing.T) {
IsAdmin: true,
},
resource: &talkgroups.Talkgroup{},
action: rbac.ActionUpdate,
action: entities.ActionUpdate,
expectErr: nil,
},
{
@ -45,7 +47,7 @@ func TestRBAC(t *testing.T) {
Name: "test incident",
Owner: 4,
},
action: rbac.ActionUpdate,
action: entities.ActionUpdate,
expectErr: nil,
},
{
@ -57,7 +59,7 @@ func TestRBAC(t *testing.T) {
Name: "test incident",
Owner: 4,
},
action: rbac.ActionUpdate,
action: entities.ActionUpdate,
expectErr: errors.New(`access denied for Action: "update" on Resource: "Incident"`),
},
{
@ -69,7 +71,7 @@ func TestRBAC(t *testing.T) {
Name: "test incident",
Owner: 2,
},
action: rbac.ActionUpdate,
action: entities.ActionUpdate,
expectErr: nil,
},
{
@ -81,7 +83,7 @@ func TestRBAC(t *testing.T) {
Name: "test incident",
Owner: 6,
},
action: rbac.ActionDelete,
action: entities.ActionDelete,
expectErr: errors.New(`access denied for Action: "delete" on Resource: "Incident"`),
},
{
@ -93,7 +95,7 @@ func TestRBAC(t *testing.T) {
resource: &calls.Call{
Submitter: common.PtrTo(users.UserID(4)),
},
action: rbac.ActionUpdate,
action: entities.ActionUpdate,
expectErr: nil,
},
{
@ -104,7 +106,7 @@ func TestRBAC(t *testing.T) {
resource: &calls.Call{
Submitter: common.PtrTo(users.UserID(4)),
},
action: rbac.ActionUpdate,
action: entities.ActionUpdate,
expectErr: errors.New(`access denied for Action: "update" on Resource: "Call"`),
},
{
@ -115,7 +117,7 @@ func TestRBAC(t *testing.T) {
resource: &calls.Call{
Submitter: common.PtrTo(users.UserID(2)),
},
action: rbac.ActionUpdate,
action: entities.ActionUpdate,
expectErr: nil,
},
{
@ -126,7 +128,7 @@ func TestRBAC(t *testing.T) {
resource: &calls.Call{
Submitter: nil,
},
action: rbac.ActionUpdate,
action: entities.ActionUpdate,
expectErr: errors.New(`access denied for Action: "update" on Resource: "Call"`),
},
{
@ -137,7 +139,7 @@ func TestRBAC(t *testing.T) {
resource: &calls.Call{
Submitter: common.PtrTo(users.UserID(6)),
},
action: rbac.ActionDelete,
action: entities.ActionDelete,
expectErr: errors.New(`access denied for Action: "delete" on Resource: "Call"`),
},
{
@ -148,7 +150,7 @@ func TestRBAC(t *testing.T) {
resource: &calls.Call{
Submitter: common.PtrTo(users.UserID(6)),
},
action: rbac.ActionShare,
action: entities.ActionShare,
expectErr: nil,
},
{
@ -160,7 +162,7 @@ func TestRBAC(t *testing.T) {
resource: &calls.Call{
Submitter: common.PtrTo(users.UserID(6)),
},
action: rbac.ActionShare,
action: entities.ActionShare,
expectErr: nil,
},
{
@ -171,7 +173,7 @@ func TestRBAC(t *testing.T) {
resource: &calls.Call{
Submitter: common.PtrTo(users.UserID(6)),
},
action: rbac.ActionShare,
action: entities.ActionShare,
expectErr: nil,
},
{
@ -182,7 +184,7 @@ func TestRBAC(t *testing.T) {
resource: &incidents.Incident{
Owner: users.UserID(6),
},
action: rbac.ActionShare,
action: entities.ActionShare,
expectErr: errors.New(`access denied for Action: "share" on Resource: "Incident"`),
},
{
@ -194,7 +196,7 @@ func TestRBAC(t *testing.T) {
resource: &incidents.Incident{
Owner: users.UserID(6),
},
action: rbac.ActionShare,
action: entities.ActionShare,
expectErr: nil,
},
{
@ -205,15 +207,15 @@ func TestRBAC(t *testing.T) {
resource: &incidents.Incident{
Owner: users.UserID(6),
},
action: rbac.ActionShare,
action: entities.ActionShare,
expectErr: nil,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
ctx := rbac.CtxWithSubject(context.Background(), tc.subject)
rb, err := rbac.New()
ctx := entities.CtxWithSubject(context.Background(), tc.subject)
rb, err := rbac.New(policy.Policy)
require.NoError(t, err)
sub, err := rb.Check(ctx, tc.resource, rbac.WithActions(tc.action))
if tc.expectErr != nil {

View file

@ -7,7 +7,7 @@ import (
"time"
"dynatron.me/x/stillbox/internal/forms"
"dynatron.me/x/stillbox/pkg/rbac"
"dynatron.me/x/stillbox/pkg/rbac/entities"
"dynatron.me/x/stillbox/pkg/shares"
"github.com/go-chi/chi/v5"
@ -133,7 +133,7 @@ func (sa *shareAPI) routeShare(w http.ResponseWriter, r *http.Request) {
return
}
ctx = rbac.CtxWithSubject(ctx, sh)
ctx = entities.CtxWithSubject(ctx, sh)
r = r.WithContext(ctx)
if params.SubType != nil {

View file

@ -17,6 +17,7 @@ import (
"dynatron.me/x/stillbox/pkg/nexus"
"dynatron.me/x/stillbox/pkg/notify"
"dynatron.me/x/stillbox/pkg/rbac"
"dynatron.me/x/stillbox/pkg/rbac/policy"
"dynatron.me/x/stillbox/pkg/rest"
"dynatron.me/x/stillbox/pkg/shares"
"dynatron.me/x/stillbox/pkg/sinks"
@ -80,7 +81,7 @@ func New(ctx context.Context, cfg *config.Configuration) (*Server, error) {
tgCache := tgstore.NewCache(db)
api := rest.New(cfg.BaseURL.URL())
rbacSvc, err := rbac.New()
rbacSvc, err := rbac.New(policy.Policy)
if err != nil {
return nil, err
}

View file

@ -4,7 +4,7 @@ import (
"context"
"time"
"dynatron.me/x/stillbox/pkg/rbac"
"dynatron.me/x/stillbox/pkg/rbac/entities"
"github.com/rs/zerolog/log"
)
@ -23,7 +23,7 @@ type service struct {
}
func (s *service) Go(ctx context.Context) {
ctx = rbac.CtxWithSubject(ctx, &rbac.SystemServiceSubject{Name: "share"})
ctx = entities.CtxWithSubject(ctx, &entities.SystemServiceSubject{Name: "share"})
tick := time.NewTicker(PruneInterval)

View file

@ -10,6 +10,7 @@ import (
"dynatron.me/x/stillbox/pkg/calls/callstore"
"dynatron.me/x/stillbox/pkg/incidents/incstore"
"dynatron.me/x/stillbox/pkg/rbac"
"dynatron.me/x/stillbox/pkg/rbac/entities"
"dynatron.me/x/stillbox/pkg/users"
"github.com/google/uuid"
@ -57,11 +58,11 @@ func (s *Share) GetName() string {
}
func (s *Share) GetRoles() []string {
return []string{rbac.RoleShareGuest}
return []string{entities.RoleShareGuest}
}
func (s *Share) GetResourceName() string {
return rbac.ResourceShare
return entities.ResourceShare
}
type CreateShareParams struct {
@ -88,7 +89,7 @@ func (s *service) checkEntity(ctx context.Context, sh *CreateShareParams) (*time
if err != nil {
return nil, err
}
_, err = rbac.Check(ctx, &i, rbac.WithActions(rbac.ActionShare))
_, err = rbac.Check(ctx, &i, rbac.WithActions(entities.ActionShare))
if err != nil {
return nil, err
}

View file

@ -7,6 +7,7 @@ import (
"dynatron.me/x/stillbox/internal/jsontypes"
"dynatron.me/x/stillbox/pkg/database"
"dynatron.me/x/stillbox/pkg/rbac"
"dynatron.me/x/stillbox/pkg/rbac/entities"
"dynatron.me/x/stillbox/pkg/users"
"github.com/jackc/pgx/v5"
)
@ -79,7 +80,7 @@ func (s *postgresStore) Create(ctx context.Context, share *Share) error {
}
func (s *postgresStore) Delete(ctx context.Context, id string) error {
_, err := rbac.Check(ctx, new(Share), rbac.WithActions(rbac.ActionDelete))
_, err := rbac.Check(ctx, new(Share), rbac.WithActions(entities.ActionDelete))
if err != nil {
return err
}

View file

@ -10,6 +10,7 @@ import (
"dynatron.me/x/stillbox/pkg/auth"
"dynatron.me/x/stillbox/pkg/calls"
"dynatron.me/x/stillbox/pkg/rbac"
"dynatron.me/x/stillbox/pkg/rbac/entities"
"dynatron.me/x/stillbox/pkg/users"
"github.com/go-chi/chi/v5"
"github.com/rs/zerolog/log"
@ -131,7 +132,7 @@ func (h *RdioHTTP) routeCallUpload(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
err = h.ing.Ingest(rbac.CtxWithSubject(ctx, submitterSub), call)
err = h.ing.Ingest(entities.CtxWithSubject(ctx, submitterSub), call)
if err != nil {
if rbac.ErrAccessDenied(err) != nil {
log.Error().Err(err).Msg("ingest failed")

View file

@ -122,6 +122,7 @@ func (f *TalkgroupFilter) compile(ctx context.Context) error {
if f.hasTags() { // don't bother with DB if no tags
db := database.FromCtx(ctx)
// TODO: change this to use tgstore, and make sure the context is no longer a system subject (see nexus.Go)
tagTGs, err := db.GetTalkgroupIDsByTags(ctx, f.TalkgroupTagsAny, f.TalkgroupTagsAll, f.TalkgroupTagsNot)
if err != nil {
return err

View file

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

View file

@ -12,6 +12,7 @@ import (
"dynatron.me/x/stillbox/pkg/config"
"dynatron.me/x/stillbox/pkg/database"
"dynatron.me/x/stillbox/pkg/rbac"
"dynatron.me/x/stillbox/pkg/rbac/entities"
tgsp "dynatron.me/x/stillbox/pkg/talkgroups"
"dynatron.me/x/stillbox/pkg/users"
@ -327,7 +328,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) {
_, err := rbac.Check(ctx, rbac.UseResource(rbac.ResourceTalkgroup), rbac.WithActions(rbac.ActionRead))
_, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceTalkgroup), rbac.WithActions(entities.ActionRead))
if err != nil {
return nil, err
}
@ -430,7 +431,7 @@ 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) {
_, err := rbac.Check(ctx, rbac.UseResource(rbac.ResourceTalkgroup), rbac.WithActions(rbac.ActionRead))
_, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceTalkgroup), rbac.WithActions(entities.ActionRead))
if err != nil {
return nil, err
}
@ -486,7 +487,7 @@ 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))
_, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceTalkgroup), rbac.WithActions(entities.ActionRead))
if err != nil {
return nil, err
}
@ -513,7 +514,7 @@ 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))
_, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceTalkgroup), rbac.WithActions(entities.ActionRead))
if err != nil {
return "", false
}
@ -587,7 +588,7 @@ 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))
_, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceTalkgroup), rbac.WithActions(entities.ActionDelete))
if err != nil {
return err
}
@ -609,7 +610,7 @@ 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))
_, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceTalkgroup), rbac.WithActions(entities.ActionDelete))
if err != nil {
return err
}
@ -645,7 +646,7 @@ func (t *cache) DeleteTG(ctx context.Context, id tgsp.ID) error {
}
func (t *cache) LearnTG(ctx context.Context, c *calls.Call) (*tgsp.Talkgroup, error) {
_, err := rbac.Check(ctx, rbac.UseResource(rbac.ResourceTalkgroup), rbac.WithActions(rbac.ActionCreate, rbac.ActionUpdate))
_, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceTalkgroup), rbac.WithActions(entities.ActionCreate, entities.ActionUpdate))
if err != nil {
return nil, err
}
@ -764,7 +765,7 @@ 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))
_, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceTalkgroup), rbac.WithActions(entities.ActionCreate))
if err != nil {
return err
}
@ -778,7 +779,7 @@ func (t *cache) CreateSystem(ctx context.Context, id int, name string) error {
}
func (t *cache) Tags(ctx context.Context) ([]string, error) {
_, err := rbac.Check(ctx, rbac.UseResource(rbac.ResourceTalkgroup), rbac.WithActions(rbac.ActionRead))
_, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceTalkgroup), rbac.WithActions(entities.ActionRead))
if err != nil {
return nil, err
}

View file

@ -7,6 +7,7 @@ import (
"dynatron.me/x/stillbox/pkg/database"
"dynatron.me/x/stillbox/pkg/rbac"
"dynatron.me/x/stillbox/pkg/rbac/entities"
)
type UserID int
@ -30,11 +31,11 @@ func (u UserID) IsValid() bool {
}
func From(ctx context.Context) (*User, error) {
sub := rbac.SubjectFrom(ctx)
sub := entities.SubjectFrom(ctx)
return FromSubject(sub)
}
func UserCheck(ctx context.Context, rsc rbac.Resource, actions string) (*User, error) {
func UserCheck(ctx context.Context, rsc entities.Resource, actions string) (*User, error) {
acts := strings.Split(actions, "+")
subj, err := rbac.FromCtx(ctx).Check(ctx, rsc, rbac.WithActions(acts...))
if err != nil {
@ -44,7 +45,7 @@ func UserCheck(ctx context.Context, rsc rbac.Resource, actions string) (*User, e
return FromSubject(subj)
}
func FromSubject(sub rbac.Subject) (*User, error) {
func FromSubject(sub entities.Subject) (*User, error) {
if sub == nil {
return nil, rbac.ErrBadSubject
}
@ -73,10 +74,10 @@ func (u *User) GetName() string {
func (u *User) GetRoles() []string {
r := make([]string, 1, 2)
r[0] = rbac.RoleUser
r[0] = entities.RoleUser
if u.IsAdmin {
r = append(r, rbac.RoleAdmin)
r = append(r, entities.RoleAdmin)
}
return r

View file

@ -24,3 +24,11 @@ DELETE FROM shares WHERE id = @id;
-- name: PruneShares :exec
DELETE FROM shares WHERE expiration < NOW();
-- name: GetIncidentTalkgroups :many
SELECT DISTINCT
c.system,
c.talkgroup
FROM incidents_calls ic
JOIN calls c ON (c.id = ic.call_id AND c.call_date = ic.call_date)
WHERE ic.incident_id = @incident_id;