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/config"
"dynatron.me/x/stillbox/pkg/database" "dynatron.me/x/stillbox/pkg/database"
"dynatron.me/x/stillbox/pkg/notify" "dynatron.me/x/stillbox/pkg/notify"
"dynatron.me/x/stillbox/pkg/rbac" "dynatron.me/x/stillbox/pkg/rbac/entities"
"dynatron.me/x/stillbox/pkg/sinks" "dynatron.me/x/stillbox/pkg/sinks"
"dynatron.me/x/stillbox/pkg/talkgroups" "dynatron.me/x/stillbox/pkg/talkgroups"
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore" "dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
@ -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. // Go is the alerting loop. It does not start a goroutine.
func (as *alerter) Go(ctx context.Context) { func (as *alerter) Go(ctx context.Context) {
ctx = rbac.CtxWithSubject(ctx, &rbac.SystemServiceSubject{Name: "alerter"}) ctx = entities.CtxWithSubject(ctx, &entities.SystemServiceSubject{Name: "alerter"})
err := as.startBackfill(ctx) err := as.startBackfill(ctx)
if err != nil { if err != nil {

View file

@ -7,7 +7,7 @@ import (
"time" "time"
"dynatron.me/x/stillbox/pkg/database" "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/google/uuid"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
@ -16,10 +16,10 @@ import (
type apiKeyAuth interface { type apiKeyAuth interface {
// CheckAPIKey validates the provided key and returns the API owner's users.UserID. // CheckAPIKey validates the provided key and returns the API owner's users.UserID.
// An error is returned if validation fails for any reason. // An error is returned if validation fails for any reason.
CheckAPIKey(ctx context.Context, key string) (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) keyUuid, err := uuid.Parse(key)
if err != nil { if err != nil {
log.Error().Str("apikey", key).Msg("cannot parse key") log.Error().Str("apikey", key).Msg("cannot parse key")

View file

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

View file

@ -8,7 +8,7 @@ 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/rbac/entities"
"dynatron.me/x/stillbox/pkg/talkgroups" "dynatron.me/x/stillbox/pkg/talkgroups"
"dynatron.me/x/stillbox/pkg/users" "dynatron.me/x/stillbox/pkg/users"
@ -76,7 +76,7 @@ type Call struct {
} }
func (c *Call) GetResourceName() string { func (c *Call) GetResourceName() string {
return rbac.ResourceCall return entities.ResourceCall
} }
func (c *Call) String() string { func (c *Call) String() string {

View file

@ -11,6 +11,7 @@ import (
"dynatron.me/x/stillbox/pkg/calls" "dynatron.me/x/stillbox/pkg/calls"
"dynatron.me/x/stillbox/pkg/database" "dynatron.me/x/stillbox/pkg/database"
"dynatron.me/x/stillbox/pkg/rbac" "dynatron.me/x/stillbox/pkg/rbac"
"dynatron.me/x/stillbox/pkg/rbac/entities"
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore" "dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
"dynatron.me/x/stillbox/pkg/users" "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 { 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 { if err != nil {
return err 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) { 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 { if err != nil {
return nil, err 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) { 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 { if err != nil {
return nil, err 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) { 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 { if err != nil {
return nil, 0, err return nil, 0, err
} }
@ -256,7 +257,7 @@ func (s *store) Delete(ctx context.Context, id uuid.UUID) error {
return err return err
} }
_, err = rbac.Check(ctx, &callOwn, rbac.WithActions(rbac.ActionDelete)) _, err = rbac.Check(ctx, &callOwn, rbac.WithActions(entities.ActionDelete))
if err != nil { if err != nil {
return err return err
} }

View file

@ -278,6 +278,64 @@ func (_c *Store_BulkSetTalkgroupTags_Call) RunAndReturn(run func(context.Context
return _c 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 // 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) { func (_m *Store) CleanupSweptCalls(ctx context.Context, rangeStart pgtype.Timestamptz, rangeEnd pgtype.Timestamptz) (int64, error) {
ret := _m.Called(ctx, rangeStart, rangeEnd) ret := _m.Called(ctx, rangeStart, rangeEnd)
@ -1750,6 +1808,65 @@ func (_c *Store_GetIncidentOwner_Call) RunAndReturn(run func(context.Context, uu
return _c 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 // GetShare provides a mock function with given fields: ctx, id
func (_m *Store) GetShare(ctx context.Context, id string) (database.Share, error) { func (_m *Store) GetShare(ctx context.Context, id string) (database.Share, error) {
ret := _m.Called(ctx, id) ret := _m.Called(ctx, id)

View file

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

View file

@ -13,7 +13,7 @@ import (
"dynatron.me/x/stillbox/internal/isoweek" "dynatron.me/x/stillbox/internal/isoweek"
"dynatron.me/x/stillbox/pkg/config" "dynatron.me/x/stillbox/pkg/config"
"dynatron.me/x/stillbox/pkg/database" "dynatron.me/x/stillbox/pkg/database"
"dynatron.me/x/stillbox/pkg/rbac" "dynatron.me/x/stillbox/pkg/rbac/entities"
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
@ -135,7 +135,7 @@ func New(db database.Store, cfg config.Partition) (*partman, error) {
var _ PartitionManager = (*partman)(nil) var _ PartitionManager = (*partman)(nil)
func (pm *partman) Go(ctx context.Context) { func (pm *partman) Go(ctx context.Context) {
ctx = rbac.CtxWithSubject(ctx, &rbac.SystemServiceSubject{Name: "partman"}) ctx = entities.CtxWithSubject(ctx, &entities.SystemServiceSubject{Name: "partman"})
tick := time.NewTicker(CheckInterval) tick := time.NewTicker(CheckInterval)
select { select {

View file

@ -40,6 +40,7 @@ type Querier interface {
GetIncident(ctx context.Context, id uuid.UUID) (Incident, error) GetIncident(ctx context.Context, id uuid.UUID) (Incident, error)
GetIncidentCalls(ctx context.Context, id uuid.UUID) ([]GetIncidentCallsRow, error) GetIncidentCalls(ctx context.Context, id uuid.UUID) ([]GetIncidentCallsRow, error)
GetIncidentOwner(ctx context.Context, id uuid.UUID) (int, error) 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) GetShare(ctx context.Context, id string) (Share, error)
GetSystemName(ctx context.Context, systemID int) (string, error) GetSystemName(ctx context.Context, systemID int) (string, error)
GetTalkgroup(ctx context.Context, systemID int32, tGID int32) (GetTalkgroupRow, error) GetTalkgroup(ctx context.Context, systemID int32, tGID int32) (GetTalkgroupRow, error)

View file

@ -53,6 +53,40 @@ func (q *Queries) DeleteShare(ctx context.Context, id string) error {
return err 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 const getShare = `-- name: GetShare :one
SELECT SELECT
id, id,

View file

@ -5,7 +5,7 @@ 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/rbac/entities"
"dynatron.me/x/stillbox/pkg/users" "dynatron.me/x/stillbox/pkg/users"
"github.com/google/uuid" "github.com/google/uuid"
) )
@ -23,7 +23,7 @@ type Incident struct {
} }
func (inc *Incident) GetResourceName() string { func (inc *Incident) GetResourceName() string {
return rbac.ResourceIncident return entities.ResourceIncident
} }
type IncidentCall struct { type IncidentCall struct {

View file

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

View file

@ -6,7 +6,7 @@ import (
"dynatron.me/x/stillbox/pkg/calls" "dynatron.me/x/stillbox/pkg/calls"
"dynatron.me/x/stillbox/pkg/pb" "dynatron.me/x/stillbox/pkg/pb"
"dynatron.me/x/stillbox/pkg/rbac" "dynatron.me/x/stillbox/pkg/rbac/entities"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@ -39,7 +39,7 @@ func New() *Nexus {
} }
func (n *Nexus) Go(ctx context.Context) { func (n *Nexus) Go(ctx context.Context) {
ctx = rbac.CtxWithSubject(ctx, &rbac.SystemServiceSubject{Name: "nexus"}) ctx = entities.CtxWithSubject(ctx, &entities.SystemServiceSubject{Name: "nexus"})
for { for {
select { select {
case call, ok := <-n.callCh: 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 ( import (
context "context" context "context"
rbac "dynatron.me/x/stillbox/pkg/rbac" entities "dynatron.me/x/stillbox/pkg/rbac/entities"
mock "github.com/stretchr/testify/mock" mock "github.com/stretchr/testify/mock"
rbac "dynatron.me/x/stillbox/pkg/rbac"
restrict "github.com/el-mike/restrict/v2" 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 // 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)) _va := make([]interface{}, len(opts))
for _i := range opts { for _i := range opts {
_va[_i] = opts[_i] _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") panic("no return value specified for Check")
} }
var r0 rbac.Subject var r0 entities.Subject
var r1 error 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...) 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...) r0 = rf(ctx, res, opts...)
} else { } else {
if ret.Get(0) != nil { 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 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) _c.Call.Return(_a0, _a1)
return _c 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) _c.Call.Return(run)
return _c return _c
} }

View file

@ -1,4 +1,4 @@
package rbac package policy
import ( import (
"context" "context"
@ -6,7 +6,7 @@ import (
"fmt" "fmt"
"reflect" "reflect"
"dynatron.me/x/stillbox/pkg/database" "dynatron.me/x/stillbox/pkg/incidents/incstore"
"github.com/el-mike/restrict/v2" "github.com/el-mike/restrict/v2"
"github.com/google/uuid" "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")) 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 { if err != nil {
return restrict.NewConditionNotSatisfiedError(c, r, err) return restrict.NewConditionNotSatisfiedError(c, r, err)
} }

View file

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

View file

@ -4,6 +4,8 @@ import (
"context" "context"
"errors" "errors"
"dynatron.me/x/stillbox/pkg/rbac/entities"
"github.com/el-mike/restrict/v2" "github.com/el-mike/restrict/v2"
"github.com/el-mike/restrict/v2/adapters" "github.com/el-mike/restrict/v2/adapters"
) )
@ -12,14 +14,6 @@ var (
ErrBadSubject = errors.New("bad subject in token") 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 { func ErrAccessDenied(err error) *restrict.AccessDeniedError {
if accessErr, ok := err.(*restrict.AccessDeniedError); ok { if accessErr, ok := err.(*restrict.AccessDeniedError); ok {
return accessErr return accessErr
@ -28,15 +22,6 @@ func ErrAccessDenied(err error) *restrict.AccessDeniedError {
return nil return nil
} }
func SubjectFrom(ctx context.Context) Subject {
sub, ok := ctx.Value(SubjectCtxKey).(Subject)
if ok {
return sub
}
return new(PublicSubject)
}
type rbacCtxKey string type rbacCtxKey string
const RBACCtxKey rbacCtxKey = "rbac" const RBACCtxKey rbacCtxKey = "rbac"
@ -81,17 +66,8 @@ func UseResource(rsc string) restrict.Resource {
return restrict.UseResource(rsc) return restrict.UseResource(rsc)
} }
type Subject interface {
restrict.Subject
GetName() string
}
type Resource interface {
restrict.Resource
}
type RBAC interface { 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 { type rbac struct {
@ -99,8 +75,8 @@ type rbac struct {
access *restrict.AccessManager access *restrict.AccessManager
} }
func New() (*rbac, error) { func New(pol *restrict.PolicyDefinition) (*rbac, error) {
adapter := adapters.NewInMemoryAdapter(Policy) adapter := adapters.NewInMemoryAdapter(pol)
polMan, err := restrict.NewPolicyManager(adapter, true) polMan, err := restrict.NewPolicyManager(adapter, true)
if err != nil { if err != nil {
return nil, err 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. // 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...) return FromCtx(ctx).Check(ctx, res, opts...)
} }
func (r *rbac) Check(ctx context.Context, res restrict.Resource, opts ...CheckOption) (Subject, error) { func (r *rbac) Check(ctx context.Context, res restrict.Resource, opts ...CheckOption) (entities.Subject, error) {
sub := SubjectFrom(ctx) sub := entities.SubjectFrom(ctx)
o := checkOptions{} o := checkOptions{}
for _, opt := range opts { for _, opt := range opts {
@ -139,34 +115,5 @@ func (r *rbac) Check(ctx context.Context, res restrict.Resource, opts ...CheckOp
Context: o.context, Context: o.context,
} }
err := r.access.Authorize(req) return sub, 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}
} }

View file

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

View file

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

View file

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

View file

@ -4,7 +4,7 @@ import (
"context" "context"
"time" "time"
"dynatron.me/x/stillbox/pkg/rbac" "dynatron.me/x/stillbox/pkg/rbac/entities"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@ -23,7 +23,7 @@ type service struct {
} }
func (s *service) Go(ctx context.Context) { 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) tick := time.NewTicker(PruneInterval)

View file

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

View file

@ -7,6 +7,7 @@ import (
"dynatron.me/x/stillbox/internal/jsontypes" "dynatron.me/x/stillbox/internal/jsontypes"
"dynatron.me/x/stillbox/pkg/database" "dynatron.me/x/stillbox/pkg/database"
"dynatron.me/x/stillbox/pkg/rbac" "dynatron.me/x/stillbox/pkg/rbac"
"dynatron.me/x/stillbox/pkg/rbac/entities"
"dynatron.me/x/stillbox/pkg/users" "dynatron.me/x/stillbox/pkg/users"
"github.com/jackc/pgx/v5" "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 { 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 { if err != nil {
return err return err
} }

View file

@ -10,6 +10,7 @@ import (
"dynatron.me/x/stillbox/pkg/auth" "dynatron.me/x/stillbox/pkg/auth"
"dynatron.me/x/stillbox/pkg/calls" "dynatron.me/x/stillbox/pkg/calls"
"dynatron.me/x/stillbox/pkg/rbac" "dynatron.me/x/stillbox/pkg/rbac"
"dynatron.me/x/stillbox/pkg/rbac/entities"
"dynatron.me/x/stillbox/pkg/users" "dynatron.me/x/stillbox/pkg/users"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
@ -131,7 +132,7 @@ func (h *RdioHTTP) routeCallUpload(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
err = h.ing.Ingest(rbac.CtxWithSubject(ctx, submitterSub), call) err = h.ing.Ingest(entities.CtxWithSubject(ctx, submitterSub), call)
if err != nil { if err != nil {
if rbac.ErrAccessDenied(err) != nil { if rbac.ErrAccessDenied(err) != nil {
log.Error().Err(err).Msg("ingest failed") 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 if f.hasTags() { // don't bother with DB if no tags
db := database.FromCtx(ctx) 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) tagTGs, err := db.GetTalkgroupIDsByTags(ctx, f.TalkgroupTagsAny, f.TalkgroupTagsAll, f.TalkgroupTagsNot)
if err != nil { if err != nil {
return err return err

View file

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

View file

@ -12,6 +12,7 @@ import (
"dynatron.me/x/stillbox/pkg/config" "dynatron.me/x/stillbox/pkg/config"
"dynatron.me/x/stillbox/pkg/database" "dynatron.me/x/stillbox/pkg/database"
"dynatron.me/x/stillbox/pkg/rbac" "dynatron.me/x/stillbox/pkg/rbac"
"dynatron.me/x/stillbox/pkg/rbac/entities"
tgsp "dynatron.me/x/stillbox/pkg/talkgroups" tgsp "dynatron.me/x/stillbox/pkg/talkgroups"
"dynatron.me/x/stillbox/pkg/users" "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) { 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 { if err != nil {
return nil, err 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) { 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 { if err != nil {
return nil, err 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) { 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 { if err != nil {
return nil, err 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) { 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 { if err != nil {
return "", false 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 { 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 { if err != nil {
return err 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 { 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 { if err != nil {
return err 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) { 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 { if err != nil {
return nil, err 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 { 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 { if err != nil {
return err 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) { 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 { if err != nil {
return nil, err return nil, err
} }

View file

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

View file

@ -24,3 +24,11 @@ DELETE FROM shares WHERE id = @id;
-- name: PruneShares :exec -- name: PruneShares :exec
DELETE FROM shares WHERE expiration < NOW(); 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;