Merge pull request 'Share links and RBAC improvements' (#104) from shareLinks into trunk
Reviewed-on: #104
This commit is contained in:
commit
0e5f62ce51
45 changed files with 1569 additions and 725 deletions
|
@ -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 {
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
@ -57,6 +57,7 @@ type Call struct {
|
|||
Audio []byte `json:"audio,omitempty" relayOut:"audio,omitempty" filenameField:"AudioName"`
|
||||
AudioName string `json:"audioName,omitempty" relayOut:"audioName,omitempty"`
|
||||
AudioType string `json:"audioType,omitempty" relayOut:"audioType,omitempty"`
|
||||
AudioURL *string `json:"audioURL,omitempty" relayOut:"audioURL,omitempty"`
|
||||
Duration CallDuration `json:"duration,omitempty" relayOut:"duration,omitempty"`
|
||||
DateTime time.Time `json:"call_date,omitempty" relayOut:"dateTime,omitempty"`
|
||||
Frequencies []int `json:"frequencies,omitempty" relayOut:"frequencies,omitempty"`
|
||||
|
@ -75,7 +76,7 @@ type Call struct {
|
|||
}
|
||||
|
||||
func (c *Call) GetResourceName() string {
|
||||
return rbac.ResourceCall
|
||||
return entities.ResourceCall
|
||||
}
|
||||
|
||||
func (c *Call) String() string {
|
||||
|
|
|
@ -3,6 +3,7 @@ package callstore
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"dynatron.me/x/stillbox/internal/common"
|
||||
"dynatron.me/x/stillbox/internal/jsontypes"
|
||||
|
@ -10,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"
|
||||
|
||||
|
@ -28,6 +30,9 @@ type Store interface {
|
|||
// CallAudio returns a CallAudio struct
|
||||
CallAudio(ctx context.Context, id uuid.UUID) (*calls.CallAudio, error)
|
||||
|
||||
// Call returns the call's metadata.
|
||||
Call(ctx context.Context, id uuid.UUID) (*calls.Call, error)
|
||||
|
||||
// Calls gets paginated Calls.
|
||||
Calls(ctx context.Context, p CallsParams) (calls []database.ListCallsPRow, totalCount int, err error)
|
||||
}
|
||||
|
@ -81,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
|
||||
}
|
||||
|
@ -119,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, rbac.UseResource(rbac.ResourceCall), rbac.WithActions(rbac.ActionRead))
|
||||
_, err := rbac.Check(ctx, &calls.Call{ID: id}, rbac.WithActions(entities.ActionRead))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -139,6 +144,43 @@ func (s *store) CallAudio(ctx context.Context, id uuid.UUID) (*calls.CallAudio,
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (s *store) Call(ctx context.Context, id uuid.UUID) (*calls.Call, error) {
|
||||
_, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceCall), rbac.WithActions(entities.ActionRead))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db := database.FromCtx(ctx)
|
||||
|
||||
c, err := db.GetCall(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var sub *users.UserID
|
||||
if c.Submitter != nil {
|
||||
sub = common.PtrTo(users.UserID(*c.Submitter))
|
||||
}
|
||||
|
||||
return &calls.Call{
|
||||
ID: c.ID,
|
||||
Submitter: sub,
|
||||
System: c.System,
|
||||
Talkgroup: c.Talkgroup,
|
||||
DateTime: c.CallDate.Time,
|
||||
AudioName: common.ZeroIfNil(c.AudioName),
|
||||
AudioType: common.ZeroIfNil(c.AudioType),
|
||||
AudioURL: c.AudioUrl,
|
||||
Duration: calls.CallDuration(time.Duration(common.ZeroIfNil(c.Duration)) * time.Millisecond),
|
||||
Frequency: c.Frequency,
|
||||
Frequencies: c.Frequencies,
|
||||
Patches: c.Patches,
|
||||
TalkgroupLabel: c.TGLabel,
|
||||
TalkgroupGroup: c.TGGroup,
|
||||
TGAlphaTag: c.TGAlphaTag,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type CallsParams struct {
|
||||
common.Pagination
|
||||
Direction *common.SortDirection `json:"dir"`
|
||||
|
@ -152,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
|
||||
}
|
||||
|
@ -215,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
|
||||
}
|
||||
|
|
|
@ -16,17 +16,18 @@ type Configuration struct {
|
|||
}
|
||||
|
||||
type Config struct {
|
||||
BaseURL jsontypes.URL `yaml:"baseURL"`
|
||||
DB DB `yaml:"db"`
|
||||
CORS CORS `yaml:"cors"`
|
||||
Auth Auth `yaml:"auth"`
|
||||
Alerting Alerting `yaml:"alerting"`
|
||||
Log []Logger `yaml:"log"`
|
||||
Listen string `yaml:"listen"`
|
||||
Public bool `yaml:"public"`
|
||||
RateLimit RateLimit `yaml:"rateLimit"`
|
||||
Notify Notify `yaml:"notify"`
|
||||
Relay []Relay `yaml:"relay"`
|
||||
BaseURL jsontypes.URL `yaml:"baseURL"`
|
||||
DumpRoutes bool `yaml:"dumpRoutes"`
|
||||
DB DB `yaml:"db"`
|
||||
CORS CORS `yaml:"cors"`
|
||||
Auth Auth `yaml:"auth"`
|
||||
Alerting Alerting `yaml:"alerting"`
|
||||
Log []Logger `yaml:"log"`
|
||||
Listen string `yaml:"listen"`
|
||||
Public bool `yaml:"public"`
|
||||
RateLimit RateLimit `yaml:"rateLimit"`
|
||||
Notify Notify `yaml:"notify"`
|
||||
Relay []Relay `yaml:"relay"`
|
||||
}
|
||||
|
||||
type Auth struct {
|
||||
|
|
|
@ -164,6 +164,71 @@ func (q *Queries) DeleteCall(ctx context.Context, id uuid.UUID) error {
|
|||
return err
|
||||
}
|
||||
|
||||
const getCall = `-- name: GetCall :one
|
||||
SELECT
|
||||
id,
|
||||
submitter,
|
||||
system,
|
||||
talkgroup,
|
||||
call_date,
|
||||
audio_name,
|
||||
audio_type,
|
||||
audio_url,
|
||||
duration,
|
||||
frequency,
|
||||
frequencies,
|
||||
patches,
|
||||
tg_label,
|
||||
tg_alpha_tag,
|
||||
tg_group,
|
||||
source
|
||||
FROM calls
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
type GetCallRow 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"`
|
||||
AudioType *string `json:"audio_type"`
|
||||
AudioUrl *string `json:"audio_url"`
|
||||
Duration *int32 `json:"duration"`
|
||||
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"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetCall(ctx context.Context, id uuid.UUID) (GetCallRow, error) {
|
||||
row := q.db.QueryRow(ctx, getCall, id)
|
||||
var i GetCallRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Submitter,
|
||||
&i.System,
|
||||
&i.Talkgroup,
|
||||
&i.CallDate,
|
||||
&i.AudioName,
|
||||
&i.AudioType,
|
||||
&i.AudioUrl,
|
||||
&i.Duration,
|
||||
&i.Frequency,
|
||||
&i.Frequencies,
|
||||
&i.Patches,
|
||||
&i.TGLabel,
|
||||
&i.TGAlphaTag,
|
||||
&i.TGGroup,
|
||||
&i.Source,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getCallAudioByID = `-- name: GetCallAudioByID :one
|
||||
SELECT
|
||||
c.call_date,
|
||||
|
|
|
@ -40,6 +40,21 @@ func (q *Queries) AddToIncident(ctx context.Context, incidentID uuid.UUID, callI
|
|||
return err
|
||||
}
|
||||
|
||||
const callInIncident = `-- name: CallInIncident :one
|
||||
SELECT EXISTS
|
||||
(SELECT 1 FROM incidents_calls ic
|
||||
WHERE
|
||||
ic.incident_id = $1 AND
|
||||
ic.call_id = $2)
|
||||
`
|
||||
|
||||
func (q *Queries) CallInIncident(ctx context.Context, incidentID uuid.UUID, callID uuid.UUID) (bool, error) {
|
||||
row := q.db.QueryRow(ctx, callInIncident, incidentID, callID)
|
||||
var exists bool
|
||||
err := row.Scan(&exists)
|
||||
return exists, err
|
||||
}
|
||||
|
||||
const createIncident = `-- name: CreateIncident :one
|
||||
INSERT INTO incidents (
|
||||
id,
|
||||
|
|
|
@ -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)
|
||||
|
@ -1348,6 +1406,63 @@ func (_c *Store_GetAppPrefs_Call) RunAndReturn(run func(context.Context, string,
|
|||
return _c
|
||||
}
|
||||
|
||||
// GetCall provides a mock function with given fields: ctx, id
|
||||
func (_m *Store) GetCall(ctx context.Context, id uuid.UUID) (database.GetCallRow, error) {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetCall")
|
||||
}
|
||||
|
||||
var r0 database.GetCallRow
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) (database.GetCallRow, error)); ok {
|
||||
return rf(ctx, id)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) database.GetCallRow); ok {
|
||||
r0 = rf(ctx, id)
|
||||
} else {
|
||||
r0 = ret.Get(0).(database.GetCallRow)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID) error); ok {
|
||||
r1 = rf(ctx, id)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Store_GetCall_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetCall'
|
||||
type Store_GetCall_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetCall is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - id uuid.UUID
|
||||
func (_e *Store_Expecter) GetCall(ctx interface{}, id interface{}) *Store_GetCall_Call {
|
||||
return &Store_GetCall_Call{Call: _e.mock.On("GetCall", ctx, id)}
|
||||
}
|
||||
|
||||
func (_c *Store_GetCall_Call) Run(run func(ctx context.Context, id uuid.UUID)) *Store_GetCall_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(uuid.UUID))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_GetCall_Call) Return(_a0 database.GetCallRow, _a1 error) *Store_GetCall_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_GetCall_Call) RunAndReturn(run func(context.Context, uuid.UUID) (database.GetCallRow, error)) *Store_GetCall_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetCallAudioByID provides a mock function with given fields: ctx, id
|
||||
func (_m *Store) GetCallAudioByID(ctx context.Context, id uuid.UUID) (database.GetCallAudioByIDRow, error) {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
@ -1693,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)
|
||||
|
|
|
@ -85,6 +85,7 @@ type Share struct {
|
|||
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"`
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -16,6 +16,7 @@ type Querier interface {
|
|||
AddCall(ctx context.Context, arg AddCallParams) error
|
||||
AddLearnedTalkgroup(ctx context.Context, arg AddLearnedTalkgroupParams) (Talkgroup, error)
|
||||
AddToIncident(ctx context.Context, incidentID uuid.UUID, callIds []uuid.UUID, notes [][]byte) error
|
||||
CallInIncident(ctx context.Context, incidentID uuid.UUID, callID uuid.UUID) (bool, error)
|
||||
CleanupSweptCalls(ctx context.Context, rangeStart pgtype.Timestamptz, rangeEnd pgtype.Timestamptz) (int64, error)
|
||||
CreateAPIKey(ctx context.Context, owner int, expires pgtype.Timestamp, disabled *bool) (ApiKey, error)
|
||||
CreateIncident(ctx context.Context, arg CreateIncidentParams) (Incident, error)
|
||||
|
@ -32,12 +33,14 @@ type Querier interface {
|
|||
GetAPIKey(ctx context.Context, apiKey string) (GetAPIKeyRow, error)
|
||||
GetAllTalkgroupTags(ctx context.Context) ([]string, error)
|
||||
GetAppPrefs(ctx context.Context, appName string, uid int) ([]byte, error)
|
||||
GetCall(ctx context.Context, id uuid.UUID) (GetCallRow, error)
|
||||
GetCallAudioByID(ctx context.Context, id uuid.UUID) (GetCallAudioByIDRow, error)
|
||||
GetCallSubmitter(ctx context.Context, id uuid.UUID) (*int32, error)
|
||||
GetDatabaseSize(ctx context.Context) (string, error)
|
||||
GetIncident(ctx context.Context, id uuid.UUID) (Incident, error)
|
||||
GetIncidentCalls(ctx context.Context, id uuid.UUID) ([]GetIncidentCallsRow, error)
|
||||
GetIncidentOwner(ctx context.Context, id uuid.UUID) (int, error)
|
||||
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)
|
||||
|
|
|
@ -17,15 +17,17 @@ INSERT INTO shares (
|
|||
id,
|
||||
entity_type,
|
||||
entity_id,
|
||||
entity_date,
|
||||
owner,
|
||||
expiration
|
||||
) VALUES ($1, $2, $3, $4, $5)
|
||||
) VALUES ($1, $2, $3, $4, $5, $6)
|
||||
`
|
||||
|
||||
type CreateShareParams 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"`
|
||||
}
|
||||
|
@ -35,6 +37,7 @@ func (q *Queries) CreateShare(ctx context.Context, arg CreateShareParams) error
|
|||
arg.ID,
|
||||
arg.EntityType,
|
||||
arg.EntityID,
|
||||
arg.EntityDate,
|
||||
arg.Owner,
|
||||
arg.Expiration,
|
||||
)
|
||||
|
@ -55,6 +58,7 @@ SELECT
|
|||
id,
|
||||
entity_type,
|
||||
entity_id,
|
||||
entity_date,
|
||||
owner,
|
||||
expiration
|
||||
FROM shares
|
||||
|
@ -68,6 +72,7 @@ func (q *Queries) GetShare(ctx context.Context, id string) (Share, error) {
|
|||
&i.ID,
|
||||
&i.EntityType,
|
||||
&i.EntityID,
|
||||
&i.EntityDate,
|
||||
&i.Owner,
|
||||
&i.Expiration,
|
||||
)
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
|
||||
"dynatron.me/x/stillbox/internal/jsontypes"
|
||||
"dynatron.me/x/stillbox/pkg/alerting/rules"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const addLearnedTalkgroup = `-- name: AddLearnedTalkgroup :one
|
||||
|
@ -117,6 +118,40 @@ func (q *Queries) GetAllTalkgroupTags(ctx context.Context) ([]string, error) {
|
|||
return items, nil
|
||||
}
|
||||
|
||||
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 getSystemName = `-- name: GetSystemName :one
|
||||
SELECT name FROM systems WHERE id = $1
|
||||
`
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -10,6 +10,8 @@ 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/talkgroups"
|
||||
"dynatron.me/x/stillbox/pkg/users"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
|
@ -46,9 +48,18 @@ type Store interface {
|
|||
|
||||
// DeleteIncident deletes an incident.
|
||||
DeleteIncident(ctx context.Context, id uuid.UUID) error
|
||||
|
||||
// Owner returns an incident with only the owner filled out.
|
||||
Owner(ctx context.Context, id uuid.UUID) (incidents.Incident, error)
|
||||
|
||||
// CallIn returns whether an incident is in an call
|
||||
CallIn(ctx context.Context, inc uuid.UUID, call uuid.UUID) (bool, error)
|
||||
|
||||
// TGsIn returns the talkgroups referenced by an incident as a map, primary for rbac use.
|
||||
TGsIn(ctx context.Context, inc uuid.UUID) (talkgroups.PresenceMap, error)
|
||||
}
|
||||
|
||||
type store struct {
|
||||
type postgresStore struct {
|
||||
}
|
||||
|
||||
type storeCtxKey string
|
||||
|
@ -69,10 +80,10 @@ func FromCtx(ctx context.Context) Store {
|
|||
}
|
||||
|
||||
func NewStore() Store {
|
||||
return &store{}
|
||||
return &postgresStore{}
|
||||
}
|
||||
|
||||
func (s *store) CreateIncident(ctx context.Context, inc incidents.Incident) (*incidents.Incident, error) {
|
||||
func (s *postgresStore) CreateIncident(ctx context.Context, inc incidents.Incident) (*incidents.Incident, error) {
|
||||
user, err := users.UserCheck(ctx, new(incidents.Incident), "create")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -131,13 +142,13 @@ func (s *store) CreateIncident(ctx context.Context, inc incidents.Incident) (*in
|
|||
return &inc, nil
|
||||
}
|
||||
|
||||
func (s *store) AddRemoveIncidentCalls(ctx context.Context, incidentID uuid.UUID, addCallIDs []uuid.UUID, notes []byte, removeCallIDs []uuid.UUID) error {
|
||||
inc, err := s.getIncidentOwner(ctx, incidentID)
|
||||
func (s *postgresStore) AddRemoveIncidentCalls(ctx context.Context, incidentID uuid.UUID, addCallIDs []uuid.UUID, notes []byte, removeCallIDs []uuid.UUID) error {
|
||||
inc, err := s.Owner(ctx, incidentID)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
@ -169,8 +180,8 @@ func (s *store) AddRemoveIncidentCalls(ctx context.Context, incidentID uuid.UUID
|
|||
}, pgx.TxOptions{})
|
||||
}
|
||||
|
||||
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))
|
||||
func (s *postgresStore) Incidents(ctx context.Context, p IncidentsParams) (incs []Incident, totalCount int, err error) {
|
||||
_, err = rbac.Check(ctx, new(incidents.Incident), rbac.WithActions(entities.ActionRead))
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
@ -274,8 +285,8 @@ func fromDBCalls(d []database.GetIncidentCallsRow) []incidents.IncidentCall {
|
|||
return r
|
||||
}
|
||||
|
||||
func (s *store) Incident(ctx context.Context, id uuid.UUID) (*incidents.Incident, error) {
|
||||
_, err := rbac.Check(ctx, new(incidents.Incident), rbac.WithActions(rbac.ActionRead))
|
||||
func (s *postgresStore) Incident(ctx context.Context, id uuid.UUID) (*incidents.Incident, error) {
|
||||
_, err := rbac.Check(ctx, &incidents.Incident{ID: id}, rbac.WithActions(entities.ActionRead))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -325,13 +336,13 @@ func (uip UpdateIncidentParams) toDBUIP(id uuid.UUID) database.UpdateIncidentPar
|
|||
}
|
||||
}
|
||||
|
||||
func (s *store) UpdateIncident(ctx context.Context, id uuid.UUID, p UpdateIncidentParams) (*incidents.Incident, error) {
|
||||
ckinc, err := s.getIncidentOwner(ctx, id)
|
||||
func (s *postgresStore) UpdateIncident(ctx context.Context, id uuid.UUID, p UpdateIncidentParams) (*incidents.Incident, error) {
|
||||
ckinc, err := s.Owner(ctx, id)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
@ -348,13 +359,13 @@ func (s *store) UpdateIncident(ctx context.Context, id uuid.UUID, p UpdateIncide
|
|||
return &inc, nil
|
||||
}
|
||||
|
||||
func (s *store) DeleteIncident(ctx context.Context, id uuid.UUID) error {
|
||||
inc, err := s.getIncidentOwner(ctx, id)
|
||||
func (s *postgresStore) DeleteIncident(ctx context.Context, id uuid.UUID) error {
|
||||
inc, err := s.Owner(ctx, id)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
@ -362,11 +373,39 @@ func (s *store) DeleteIncident(ctx context.Context, id uuid.UUID) error {
|
|||
return database.FromCtx(ctx).DeleteIncident(ctx, id)
|
||||
}
|
||||
|
||||
func (s *store) UpdateNotes(ctx context.Context, incidentID uuid.UUID, callID uuid.UUID, notes []byte) error {
|
||||
func (s *postgresStore) UpdateNotes(ctx context.Context, incidentID uuid.UUID, callID uuid.UUID, notes []byte) error {
|
||||
return database.FromCtx(ctx).UpdateCallIncidentNotes(ctx, notes, incidentID, callID)
|
||||
}
|
||||
|
||||
func (s *store) getIncidentOwner(ctx context.Context, id uuid.UUID) (incidents.Incident, error) {
|
||||
func (s *postgresStore) Owner(ctx context.Context, id uuid.UUID) (incidents.Incident, error) {
|
||||
owner, err := database.FromCtx(ctx).GetIncidentOwner(ctx, id)
|
||||
return incidents.Incident{ID: id, Owner: users.UserID(owner)}, err
|
||||
}
|
||||
|
||||
func (s *postgresStore) CallIn(ctx context.Context, inc uuid.UUID, call uuid.UUID) (bool, error) {
|
||||
db := database.FromCtx(ctx)
|
||||
return db.CallInIncident(ctx, inc, call)
|
||||
}
|
||||
|
||||
func (s *postgresStore) TGsIn(ctx context.Context, id uuid.UUID) (talkgroups.PresenceMap, error) {
|
||||
_, err := rbac.Check(ctx, &incidents.Incident{ID: id}, rbac.WithActions(entities.ActionRead))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db := database.FromCtx(ctx)
|
||||
tgs, err := db.GetIncidentTalkgroups(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m := make(talkgroups.PresenceMap, len(tgs))
|
||||
for _, t := range tgs {
|
||||
m.Put(talkgroups.ID{
|
||||
System: uint32(t.System),
|
||||
Talkgroup: uint32(t.Talkgroup),
|
||||
})
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
79
pkg/rbac/entities/entities.go
Normal file
79
pkg/rbac/entities/entities.go
Normal 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}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
196
pkg/rbac/policy/conditions.go
Normal file
196
pkg/rbac/policy/conditions.go
Normal file
|
@ -0,0 +1,196 @@
|
|||
package policy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"dynatron.me/x/stillbox/pkg/incidents/incstore"
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups"
|
||||
|
||||
"github.com/el-mike/restrict/v2"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
SubmitterEqualConditionType = "SUBMITTER_EQUAL"
|
||||
InMapConditionType = "IN_MAP"
|
||||
CallInIncidentConditionType = "CALL_IN_INCIDENT"
|
||||
TGInIncidentConditionType = "TG_IN_INCIDENT"
|
||||
)
|
||||
|
||||
type TGInIncidentCondition struct {
|
||||
ID string `json:"name,omitempty" yaml:"name,omitempty"`
|
||||
TG *restrict.ValueDescriptor `json:"tg" yaml:"tg"`
|
||||
Incident *restrict.ValueDescriptor `json:"incident" yaml:"incident"`
|
||||
}
|
||||
|
||||
func (*TGInIncidentCondition) Type() string {
|
||||
return TGInIncidentConditionType
|
||||
}
|
||||
|
||||
func (c *TGInIncidentCondition) Check(r *restrict.AccessRequest) error {
|
||||
tgVID, err := c.TG.GetValue(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
incVID, err := c.Incident.GetValue(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, hasCtx := r.Context["ctx"].(context.Context)
|
||||
if !hasCtx {
|
||||
return restrict.NewConditionNotSatisfiedError(c, r, fmt.Errorf("no context provided"))
|
||||
}
|
||||
|
||||
incID, isUUID := incVID.(uuid.UUID)
|
||||
if !isUUID {
|
||||
return restrict.NewConditionNotSatisfiedError(c, r, errors.New("incident ID is not UUID"))
|
||||
}
|
||||
|
||||
tgID, isTGID := tgVID.(talkgroups.ID)
|
||||
if !isTGID {
|
||||
return restrict.NewConditionNotSatisfiedError(c, r, errors.New("tg ID is not TGID"))
|
||||
}
|
||||
|
||||
// XXX: this should instead come from the access request context, for better reuse upstream
|
||||
tgm, err := incstore.FromCtx(ctx).TGsIn(ctx, incID)
|
||||
if err != nil {
|
||||
return restrict.NewConditionNotSatisfiedError(c, r, err)
|
||||
}
|
||||
|
||||
if !tgm.Has(tgID) {
|
||||
return restrict.NewConditionNotSatisfiedError(c, r, fmt.Errorf(`tg "%v" not in incident "%v"`, tgID, incID))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type CallInIncidentCondition struct {
|
||||
ID string `json:"name,omitempty" yaml:"name,omitempty"`
|
||||
Call *restrict.ValueDescriptor `json:"call" yaml:"call"`
|
||||
Incident *restrict.ValueDescriptor `json:"incident" yaml:"incident"`
|
||||
}
|
||||
|
||||
func (*CallInIncidentCondition) Type() string {
|
||||
return CallInIncidentConditionType
|
||||
}
|
||||
|
||||
func (c *CallInIncidentCondition) Check(r *restrict.AccessRequest) error {
|
||||
callVID, err := c.Call.GetValue(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
incVID, err := c.Incident.GetValue(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, hasCtx := r.Context["ctx"].(context.Context)
|
||||
if !hasCtx {
|
||||
return restrict.NewConditionNotSatisfiedError(c, r, fmt.Errorf("no context provided"))
|
||||
}
|
||||
|
||||
incID, isUUID := incVID.(uuid.UUID)
|
||||
if !isUUID {
|
||||
return restrict.NewConditionNotSatisfiedError(c, r, errors.New("incident ID is not UUID"))
|
||||
}
|
||||
|
||||
callID, isUUID := callVID.(uuid.UUID)
|
||||
if !isUUID {
|
||||
return restrict.NewConditionNotSatisfiedError(c, r, errors.New("call ID is not UUID"))
|
||||
}
|
||||
|
||||
inCall, err := incstore.FromCtx(ctx).CallIn(ctx, incID, callID)
|
||||
if err != nil {
|
||||
return restrict.NewConditionNotSatisfiedError(c, r, err)
|
||||
}
|
||||
|
||||
if !inCall {
|
||||
return restrict.NewConditionNotSatisfiedError(c, r, fmt.Errorf(`call "%v" not in incident "%v"`, callID, incID))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type SubmitterEqualCondition struct {
|
||||
ID string `json:"name,omitempty" yaml:"name,omitempty"`
|
||||
Left *restrict.ValueDescriptor `json:"left" yaml:"left"`
|
||||
Right *restrict.ValueDescriptor `json:"right" yaml:"right"`
|
||||
}
|
||||
|
||||
func (s *SubmitterEqualCondition) Type() string {
|
||||
return SubmitterEqualConditionType
|
||||
}
|
||||
|
||||
func (c *SubmitterEqualCondition) Check(r *restrict.AccessRequest) error {
|
||||
left, err := c.Left.GetValue(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
right, err := c.Right.GetValue(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lVal := reflect.ValueOf(left)
|
||||
rVal := reflect.ValueOf(right)
|
||||
|
||||
// deref Left. this is the difference between us and EqualCondition
|
||||
for lVal.Kind() == reflect.Pointer {
|
||||
lVal = lVal.Elem()
|
||||
}
|
||||
|
||||
if !lVal.IsValid() || !reflect.DeepEqual(rVal.Interface(), lVal.Interface()) {
|
||||
return restrict.NewConditionNotSatisfiedError(c, r, fmt.Errorf("values \"%v\" and \"%v\" are not equal", left, right))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func SubmitterEqualConditionFactory() restrict.Condition {
|
||||
return new(SubmitterEqualCondition)
|
||||
}
|
||||
|
||||
type InMapCondition[K comparable, V any] struct {
|
||||
ID string `json:"name,omitempty" yaml:"name,omitempty"`
|
||||
Key *restrict.ValueDescriptor `json:"key" yaml:"key"`
|
||||
Map *restrict.ValueDescriptor `json:"map" yaml:"map"`
|
||||
}
|
||||
|
||||
func (s *InMapCondition[K, V]) Type() string {
|
||||
return SubmitterEqualConditionType
|
||||
}
|
||||
|
||||
func InMapConditionFactory[K comparable, V any]() restrict.Condition {
|
||||
return new(InMapCondition[K, V])
|
||||
}
|
||||
|
||||
func (c *InMapCondition[K, V]) Check(r *restrict.AccessRequest) error {
|
||||
cKey, err := c.Key.GetValue(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cMap, err := c.Map.GetValue(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
key := cKey.(K)
|
||||
|
||||
if _, in := cMap.(map[K]V)[key]; !in {
|
||||
return restrict.NewConditionNotSatisfiedError(c, r, fmt.Errorf("key '%v' not in map", key))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
_ = restrict.RegisterConditionFactory(SubmitterEqualConditionType, SubmitterEqualConditionFactory)
|
||||
}
|
243
pkg/rbac/policy/policy.go
Normal file
243
pkg/rbac/policy/policy.go
Normal file
|
@ -0,0 +1,243 @@
|
|||
package policy
|
||||
|
||||
import (
|
||||
"dynatron.me/x/stillbox/pkg/rbac/entities"
|
||||
|
||||
"github.com/el-mike/restrict/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
PresetUpdateOwn = "updateOwn"
|
||||
PresetDeleteOwn = "deleteOwn"
|
||||
PresetReadShared = "readShared"
|
||||
PresetReadSharedInMap = "readSharedInMap"
|
||||
PresetShareOwn = "shareOwn"
|
||||
|
||||
PresetUpdateSubmitter = "updateSubmitter"
|
||||
PresetDeleteSubmitter = "deleteSubmitter"
|
||||
PresetShareSubmitter = "shareSubmitter"
|
||||
PresetReadInSharedIncident = "readInSharedIncident"
|
||||
)
|
||||
|
||||
var Policy = &restrict.PolicyDefinition{
|
||||
Roles: restrict.Roles{
|
||||
entities.RoleUser: {
|
||||
Description: "An authenticated user",
|
||||
Grants: restrict.GrantsMap{
|
||||
entities.ResourceIncident: {
|
||||
&restrict.Permission{Action: entities.ActionRead},
|
||||
&restrict.Permission{Action: entities.ActionCreate},
|
||||
&restrict.Permission{Preset: PresetUpdateOwn},
|
||||
&restrict.Permission{Preset: PresetDeleteOwn},
|
||||
&restrict.Permission{Preset: PresetShareOwn},
|
||||
},
|
||||
entities.ResourceCall: {
|
||||
&restrict.Permission{Action: entities.ActionRead},
|
||||
&restrict.Permission{Action: entities.ActionCreate},
|
||||
&restrict.Permission{Preset: PresetUpdateSubmitter},
|
||||
&restrict.Permission{Preset: PresetDeleteSubmitter},
|
||||
&restrict.Permission{Action: entities.ActionShare},
|
||||
},
|
||||
entities.ResourceTalkgroup: {
|
||||
&restrict.Permission{Action: entities.ActionRead},
|
||||
},
|
||||
entities.ResourceShare: {
|
||||
&restrict.Permission{Action: entities.ActionRead},
|
||||
&restrict.Permission{Action: entities.ActionCreate},
|
||||
&restrict.Permission{Preset: PresetUpdateOwn},
|
||||
&restrict.Permission{Preset: PresetDeleteOwn},
|
||||
},
|
||||
},
|
||||
},
|
||||
entities.RoleSubmitter: {
|
||||
Description: "A role that can submit calls",
|
||||
Grants: restrict.GrantsMap{
|
||||
entities.ResourceCall: {
|
||||
&restrict.Permission{Action: entities.ActionCreate},
|
||||
},
|
||||
entities.ResourceTalkgroup: {
|
||||
// for learning TGs
|
||||
&restrict.Permission{Action: entities.ActionCreate},
|
||||
&restrict.Permission{Action: entities.ActionUpdate},
|
||||
},
|
||||
},
|
||||
},
|
||||
entities.RoleShareGuest: {
|
||||
Description: "Someone who has a valid share link",
|
||||
Grants: restrict.GrantsMap{
|
||||
entities.ResourceCall: {
|
||||
&restrict.Permission{Preset: PresetReadShared},
|
||||
&restrict.Permission{Preset: PresetReadInSharedIncident},
|
||||
},
|
||||
entities.ResourceIncident: {
|
||||
&restrict.Permission{Preset: PresetReadShared},
|
||||
},
|
||||
entities.ResourceTalkgroup: {
|
||||
&restrict.Permission{Action: entities.ActionRead},
|
||||
},
|
||||
},
|
||||
},
|
||||
entities.RoleAdmin: {
|
||||
Parents: []string{entities.RoleUser},
|
||||
Grants: restrict.GrantsMap{
|
||||
entities.ResourceIncident: {
|
||||
&restrict.Permission{Action: entities.ActionUpdate},
|
||||
&restrict.Permission{Action: entities.ActionDelete},
|
||||
&restrict.Permission{Action: entities.ActionShare},
|
||||
},
|
||||
entities.ResourceCall: {
|
||||
&restrict.Permission{Action: entities.ActionUpdate},
|
||||
&restrict.Permission{Action: entities.ActionDelete},
|
||||
&restrict.Permission{Action: entities.ActionShare},
|
||||
},
|
||||
entities.ResourceTalkgroup: {
|
||||
&restrict.Permission{Action: entities.ActionUpdate},
|
||||
&restrict.Permission{Action: entities.ActionCreate},
|
||||
&restrict.Permission{Action: entities.ActionDelete},
|
||||
},
|
||||
},
|
||||
},
|
||||
entities.RoleSystem: {
|
||||
Parents: []string{entities.RoleSystem},
|
||||
},
|
||||
entities.RolePublic: {
|
||||
/*
|
||||
Grants: restrict.GrantsMap{
|
||||
entities.ResourceShare: {
|
||||
&restrict.Permission{Action: entities.ActionRead},
|
||||
},
|
||||
},
|
||||
*/
|
||||
},
|
||||
},
|
||||
PermissionPresets: restrict.PermissionPresets{
|
||||
PresetUpdateOwn: &restrict.Permission{
|
||||
Action: entities.ActionUpdate,
|
||||
Conditions: restrict.Conditions{
|
||||
&restrict.EqualCondition{
|
||||
ID: "isOwner",
|
||||
Left: &restrict.ValueDescriptor{
|
||||
Source: restrict.ResourceField,
|
||||
Field: "Owner",
|
||||
},
|
||||
Right: &restrict.ValueDescriptor{
|
||||
Source: restrict.SubjectField,
|
||||
Field: "ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
PresetDeleteOwn: &restrict.Permission{
|
||||
Action: entities.ActionDelete,
|
||||
Conditions: restrict.Conditions{
|
||||
&restrict.EqualCondition{
|
||||
ID: "isOwner",
|
||||
Left: &restrict.ValueDescriptor{
|
||||
Source: restrict.ResourceField,
|
||||
Field: "Owner",
|
||||
},
|
||||
Right: &restrict.ValueDescriptor{
|
||||
Source: restrict.SubjectField,
|
||||
Field: "ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
PresetShareOwn: &restrict.Permission{
|
||||
Action: entities.ActionShare,
|
||||
Conditions: restrict.Conditions{
|
||||
&restrict.EqualCondition{
|
||||
ID: "isOwner",
|
||||
Left: &restrict.ValueDescriptor{
|
||||
Source: restrict.ResourceField,
|
||||
Field: "Owner",
|
||||
},
|
||||
Right: &restrict.ValueDescriptor{
|
||||
Source: restrict.SubjectField,
|
||||
Field: "ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
PresetUpdateSubmitter: &restrict.Permission{
|
||||
Action: entities.ActionUpdate,
|
||||
Conditions: restrict.Conditions{
|
||||
&SubmitterEqualCondition{
|
||||
ID: "isSubmitter",
|
||||
Left: &restrict.ValueDescriptor{
|
||||
Source: restrict.ResourceField,
|
||||
Field: "Submitter",
|
||||
},
|
||||
Right: &restrict.ValueDescriptor{
|
||||
Source: restrict.SubjectField,
|
||||
Field: "ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
PresetDeleteSubmitter: &restrict.Permission{
|
||||
Action: entities.ActionDelete,
|
||||
Conditions: restrict.Conditions{
|
||||
&SubmitterEqualCondition{
|
||||
ID: "isSubmitter",
|
||||
Left: &restrict.ValueDescriptor{
|
||||
Source: restrict.ResourceField,
|
||||
Field: "Submitter",
|
||||
},
|
||||
Right: &restrict.ValueDescriptor{
|
||||
Source: restrict.SubjectField,
|
||||
Field: "ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
PresetShareSubmitter: &restrict.Permission{
|
||||
Action: entities.ActionShare,
|
||||
Conditions: restrict.Conditions{
|
||||
&SubmitterEqualCondition{
|
||||
ID: "isSubmitter",
|
||||
Left: &restrict.ValueDescriptor{
|
||||
Source: restrict.ResourceField,
|
||||
Field: "Submitter",
|
||||
},
|
||||
Right: &restrict.ValueDescriptor{
|
||||
Source: restrict.SubjectField,
|
||||
Field: "ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
PresetReadShared: &restrict.Permission{
|
||||
Action: entities.ActionRead,
|
||||
Conditions: restrict.Conditions{
|
||||
&restrict.EqualCondition{
|
||||
ID: "isOwner",
|
||||
Left: &restrict.ValueDescriptor{
|
||||
Source: restrict.ResourceField,
|
||||
Field: "ID",
|
||||
},
|
||||
Right: &restrict.ValueDescriptor{
|
||||
Source: restrict.SubjectField,
|
||||
Field: "EntityID",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
PresetReadInSharedIncident: &restrict.Permission{
|
||||
Action: entities.ActionRead,
|
||||
Conditions: restrict.Conditions{
|
||||
&CallInIncidentCondition{
|
||||
ID: "callInIncident",
|
||||
Call: &restrict.ValueDescriptor{
|
||||
Source: restrict.ResourceField,
|
||||
Field: "ID",
|
||||
},
|
||||
Incident: &restrict.ValueDescriptor{
|
||||
Source: restrict.SubjectField,
|
||||
Field: "EntityID",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
330
pkg/rbac/rbac.go
330
pkg/rbac/rbac.go
|
@ -3,53 +3,17 @@ package rbac
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"dynatron.me/x/stillbox/pkg/rbac/entities"
|
||||
|
||||
"github.com/el-mike/restrict/v2"
|
||||
"github.com/el-mike/restrict/v2/adapters"
|
||||
)
|
||||
|
||||
const (
|
||||
RoleUser = "User"
|
||||
RoleSubmitter = "Submitter"
|
||||
RoleAdmin = "Admin"
|
||||
RoleSystem = "System"
|
||||
RolePublic = "Public"
|
||||
RoleShareGuest = "ShareGuest"
|
||||
|
||||
ResourceCall = "Call"
|
||||
ResourceIncident = "Incident"
|
||||
ResourceTalkgroup = "Talkgroup"
|
||||
ResourceAlert = "Alert"
|
||||
ResourceShare = "Share"
|
||||
ResourceAPIKey = "APIKey"
|
||||
|
||||
ActionRead = "read"
|
||||
ActionCreate = "create"
|
||||
ActionUpdate = "update"
|
||||
ActionDelete = "delete"
|
||||
|
||||
PresetUpdateOwn = "updateOwn"
|
||||
PresetDeleteOwn = "deleteOwn"
|
||||
PresetReadShared = "readShared"
|
||||
|
||||
PresetUpdateSubmitter = "updateSubmitter"
|
||||
PresetDeleteSubmitter = "deleteSubmitter"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrBadSubject = errors.New("bad subject in token")
|
||||
)
|
||||
|
||||
type subjectContextKey string
|
||||
|
||||
const SubjectCtxKey subjectContextKey = "sub"
|
||||
|
||||
func CtxWithSubject(ctx context.Context, sub Subject) context.Context {
|
||||
return context.WithValue(ctx, SubjectCtxKey, sub)
|
||||
}
|
||||
|
||||
func ErrAccessDenied(err error) *restrict.AccessDeniedError {
|
||||
if accessErr, ok := err.(*restrict.AccessDeniedError); ok {
|
||||
return accessErr
|
||||
|
@ -58,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"
|
||||
|
@ -88,176 +43,6 @@ var (
|
|||
ErrNotAuthorized = errors.New("not authorized")
|
||||
)
|
||||
|
||||
var policy = &restrict.PolicyDefinition{
|
||||
Roles: restrict.Roles{
|
||||
RoleUser: {
|
||||
Description: "An authenticated user",
|
||||
Grants: restrict.GrantsMap{
|
||||
ResourceIncident: {
|
||||
&restrict.Permission{Action: ActionRead},
|
||||
&restrict.Permission{Action: ActionCreate},
|
||||
&restrict.Permission{Preset: PresetUpdateOwn},
|
||||
&restrict.Permission{Preset: PresetDeleteOwn},
|
||||
},
|
||||
ResourceCall: {
|
||||
&restrict.Permission{Action: ActionRead},
|
||||
&restrict.Permission{Action: ActionCreate},
|
||||
&restrict.Permission{Preset: PresetUpdateSubmitter},
|
||||
&restrict.Permission{Preset: PresetDeleteSubmitter},
|
||||
},
|
||||
ResourceTalkgroup: {
|
||||
&restrict.Permission{Action: ActionRead},
|
||||
},
|
||||
ResourceShare: {
|
||||
&restrict.Permission{Action: ActionRead},
|
||||
&restrict.Permission{Action: ActionCreate},
|
||||
&restrict.Permission{Preset: PresetUpdateOwn},
|
||||
&restrict.Permission{Preset: PresetDeleteOwn},
|
||||
},
|
||||
},
|
||||
},
|
||||
RoleSubmitter: {
|
||||
Description: "A role that can submit calls",
|
||||
Grants: restrict.GrantsMap{
|
||||
ResourceCall: {
|
||||
&restrict.Permission{Action: ActionCreate},
|
||||
},
|
||||
ResourceTalkgroup: {
|
||||
// for learning TGs
|
||||
&restrict.Permission{Action: ActionCreate},
|
||||
&restrict.Permission{Action: ActionUpdate},
|
||||
},
|
||||
},
|
||||
},
|
||||
RoleShareGuest: {
|
||||
Description: "Someone who has a valid share link",
|
||||
Grants: restrict.GrantsMap{
|
||||
ResourceCall: {
|
||||
&restrict.Permission{Preset: PresetReadShared},
|
||||
},
|
||||
ResourceIncident: {
|
||||
&restrict.Permission{Preset: PresetReadShared},
|
||||
},
|
||||
ResourceTalkgroup: {
|
||||
&restrict.Permission{Action: ActionRead},
|
||||
},
|
||||
},
|
||||
},
|
||||
RoleAdmin: {
|
||||
Parents: []string{RoleUser},
|
||||
Grants: restrict.GrantsMap{
|
||||
ResourceIncident: {
|
||||
&restrict.Permission{Action: ActionUpdate},
|
||||
&restrict.Permission{Action: ActionDelete},
|
||||
},
|
||||
ResourceCall: {
|
||||
&restrict.Permission{Action: ActionUpdate},
|
||||
&restrict.Permission{Action: ActionDelete},
|
||||
},
|
||||
ResourceTalkgroup: {
|
||||
&restrict.Permission{Action: ActionUpdate},
|
||||
&restrict.Permission{Action: ActionCreate},
|
||||
&restrict.Permission{Action: ActionDelete},
|
||||
},
|
||||
},
|
||||
},
|
||||
RoleSystem: {
|
||||
Parents: []string{RoleSystem},
|
||||
},
|
||||
RolePublic: {
|
||||
/*
|
||||
Grants: restrict.GrantsMap{
|
||||
ResourceShare: {
|
||||
&restrict.Permission{Action: ActionRead},
|
||||
},
|
||||
},
|
||||
*/
|
||||
},
|
||||
},
|
||||
PermissionPresets: restrict.PermissionPresets{
|
||||
PresetUpdateOwn: &restrict.Permission{
|
||||
Action: ActionUpdate,
|
||||
Conditions: restrict.Conditions{
|
||||
&restrict.EqualCondition{
|
||||
ID: "isOwner",
|
||||
Left: &restrict.ValueDescriptor{
|
||||
Source: restrict.ResourceField,
|
||||
Field: "Owner",
|
||||
},
|
||||
Right: &restrict.ValueDescriptor{
|
||||
Source: restrict.SubjectField,
|
||||
Field: "ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
PresetDeleteOwn: &restrict.Permission{
|
||||
Action: ActionDelete,
|
||||
Conditions: restrict.Conditions{
|
||||
&restrict.EqualCondition{
|
||||
ID: "isOwner",
|
||||
Left: &restrict.ValueDescriptor{
|
||||
Source: restrict.ResourceField,
|
||||
Field: "Owner",
|
||||
},
|
||||
Right: &restrict.ValueDescriptor{
|
||||
Source: restrict.SubjectField,
|
||||
Field: "ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
PresetUpdateSubmitter: &restrict.Permission{
|
||||
Action: ActionUpdate,
|
||||
Conditions: restrict.Conditions{
|
||||
&SubmitterEqualCondition{
|
||||
ID: "isSubmitter",
|
||||
Left: &restrict.ValueDescriptor{
|
||||
Source: restrict.ResourceField,
|
||||
Field: "Submitter",
|
||||
},
|
||||
Right: &restrict.ValueDescriptor{
|
||||
Source: restrict.SubjectField,
|
||||
Field: "ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
PresetDeleteSubmitter: &restrict.Permission{
|
||||
Action: ActionDelete,
|
||||
Conditions: restrict.Conditions{
|
||||
&SubmitterEqualCondition{
|
||||
ID: "isSubmitter",
|
||||
Left: &restrict.ValueDescriptor{
|
||||
Source: restrict.ResourceField,
|
||||
Field: "Submitter",
|
||||
},
|
||||
Right: &restrict.ValueDescriptor{
|
||||
Source: restrict.SubjectField,
|
||||
Field: "ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
PresetReadShared: &restrict.Permission{
|
||||
Action: ActionRead,
|
||||
Conditions: restrict.Conditions{
|
||||
&restrict.EqualCondition{
|
||||
ID: "isOwner",
|
||||
Left: &restrict.ValueDescriptor{
|
||||
Source: restrict.ContextField,
|
||||
Field: "Owner",
|
||||
},
|
||||
Right: &restrict.ValueDescriptor{
|
||||
Source: restrict.SubjectField,
|
||||
Field: "ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type checkOptions struct {
|
||||
actions []string
|
||||
context restrict.Context
|
||||
|
@ -281,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 {
|
||||
|
@ -299,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
|
||||
|
@ -314,18 +90,24 @@ 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 {
|
||||
opt(&o)
|
||||
}
|
||||
|
||||
if o.context == nil {
|
||||
o.context = make(restrict.Context)
|
||||
}
|
||||
|
||||
o.context["ctx"] = ctx
|
||||
|
||||
req := &restrict.AccessRequest{
|
||||
Subject: sub,
|
||||
Resource: res,
|
||||
|
@ -335,87 +117,3 @@ func (r *rbac) Check(ctx context.Context, res restrict.Resource, opts ...CheckOp
|
|||
|
||||
return sub, r.access.Authorize(req)
|
||||
}
|
||||
|
||||
type ShareLinkGuest struct {
|
||||
ShareID string
|
||||
}
|
||||
|
||||
func (s *ShareLinkGuest) GetName() string {
|
||||
return "SHARE:" + s.ShareID
|
||||
}
|
||||
|
||||
func (s *ShareLinkGuest) GetRoles() []string {
|
||||
return []string{RoleShareGuest}
|
||||
}
|
||||
|
||||
type PublicSubject struct {
|
||||
RemoteAddr string
|
||||
}
|
||||
|
||||
func (s *PublicSubject) GetName() string {
|
||||
return "PUBLIC:" + s.RemoteAddr
|
||||
}
|
||||
|
||||
func (s *PublicSubject) GetRoles() []string {
|
||||
return []string{RolePublic}
|
||||
}
|
||||
|
||||
type SystemServiceSubject struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (s *SystemServiceSubject) GetName() string {
|
||||
return "SYSTEM:" + s.Name
|
||||
}
|
||||
|
||||
func (s *SystemServiceSubject) GetRoles() []string {
|
||||
return []string{RoleSystem}
|
||||
}
|
||||
|
||||
const (
|
||||
SubmitterEqualConditionType = "SUBMITTER_EQUAL"
|
||||
)
|
||||
|
||||
type SubmitterEqualCondition struct {
|
||||
ID string `json:"name,omitempty" yaml:"name,omitempty"`
|
||||
Left *restrict.ValueDescriptor `json:"left" yaml:"left"`
|
||||
Right *restrict.ValueDescriptor `json:"right" yaml:"right"`
|
||||
}
|
||||
|
||||
func (s *SubmitterEqualCondition) Type() string {
|
||||
return SubmitterEqualConditionType
|
||||
}
|
||||
|
||||
func (c *SubmitterEqualCondition) Check(r *restrict.AccessRequest) error {
|
||||
left, err := c.Left.GetValue(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
right, err := c.Right.GetValue(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lVal := reflect.ValueOf(left)
|
||||
rVal := reflect.ValueOf(right)
|
||||
|
||||
// deref Left. this is the difference between us and EqualCondition
|
||||
for lVal.Kind() == reflect.Pointer {
|
||||
lVal = lVal.Elem()
|
||||
}
|
||||
|
||||
if !lVal.IsValid() || !reflect.DeepEqual(rVal.Interface(), lVal.Interface()) {
|
||||
return restrict.NewConditionNotSatisfiedError(c, r, fmt.Errorf("values \"%v\" and \"%v\" are not equal", left, right))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func SubmitterEqualConditionFactory() restrict.Condition {
|
||||
return new(SubmitterEqualCondition)
|
||||
}
|
||||
|
||||
func init() {
|
||||
restrict.RegisterConditionFactory(SubmitterEqualConditionType, SubmitterEqualConditionFactory)
|
||||
}
|
||||
|
|
|
@ -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,15 +139,83 @@ 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"`),
|
||||
},
|
||||
{
|
||||
name: "user share call not submitter",
|
||||
subject: &users.User{
|
||||
ID: 2,
|
||||
},
|
||||
resource: &calls.Call{
|
||||
Submitter: common.PtrTo(users.UserID(6)),
|
||||
},
|
||||
action: entities.ActionShare,
|
||||
expectErr: nil,
|
||||
},
|
||||
{
|
||||
name: "user share call admin",
|
||||
subject: &users.User{
|
||||
ID: 2,
|
||||
IsAdmin: true,
|
||||
},
|
||||
resource: &calls.Call{
|
||||
Submitter: common.PtrTo(users.UserID(6)),
|
||||
},
|
||||
action: entities.ActionShare,
|
||||
expectErr: nil,
|
||||
},
|
||||
{
|
||||
name: "user share call submitter",
|
||||
subject: &users.User{
|
||||
ID: 6,
|
||||
},
|
||||
resource: &calls.Call{
|
||||
Submitter: common.PtrTo(users.UserID(6)),
|
||||
},
|
||||
action: entities.ActionShare,
|
||||
expectErr: nil,
|
||||
},
|
||||
{
|
||||
name: "user share incident not owner",
|
||||
subject: &users.User{
|
||||
ID: 2,
|
||||
},
|
||||
resource: &incidents.Incident{
|
||||
Owner: users.UserID(6),
|
||||
},
|
||||
action: entities.ActionShare,
|
||||
expectErr: errors.New(`access denied for Action: "share" on Resource: "Incident"`),
|
||||
},
|
||||
{
|
||||
name: "user share incident admin",
|
||||
subject: &users.User{
|
||||
ID: 2,
|
||||
IsAdmin: true,
|
||||
},
|
||||
resource: &incidents.Incident{
|
||||
Owner: users.UserID(6),
|
||||
},
|
||||
action: entities.ActionShare,
|
||||
expectErr: nil,
|
||||
},
|
||||
{
|
||||
name: "user share incident owner",
|
||||
subject: &users.User{
|
||||
ID: 6,
|
||||
},
|
||||
resource: &incidents.Incident{
|
||||
Owner: users.UserID(6),
|
||||
},
|
||||
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 {
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
|
||||
"dynatron.me/x/stillbox/internal/common"
|
||||
"dynatron.me/x/stillbox/pkg/rbac"
|
||||
"dynatron.me/x/stillbox/pkg/shares"
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
@ -21,12 +22,41 @@ type API interface {
|
|||
Subrouter() http.Handler
|
||||
}
|
||||
|
||||
type APIRoot interface {
|
||||
API
|
||||
ShareRouter() http.Handler
|
||||
}
|
||||
|
||||
type api struct {
|
||||
baseURL url.URL
|
||||
baseURL *url.URL
|
||||
shares *shareAPI
|
||||
tgs *talkgroupAPI
|
||||
calls *callsAPI
|
||||
users *usersAPI
|
||||
incidents *incidentsAPI
|
||||
}
|
||||
|
||||
func (a *api) ShareRouter() http.Handler {
|
||||
return a.shares.RootRouter()
|
||||
}
|
||||
|
||||
func New(baseURL url.URL) *api {
|
||||
s := &api{baseURL}
|
||||
s := &api{
|
||||
baseURL: &baseURL,
|
||||
tgs: new(talkgroupAPI),
|
||||
calls: new(callsAPI),
|
||||
incidents: newIncidentsAPI(&baseURL),
|
||||
users: new(usersAPI),
|
||||
}
|
||||
s.shares = newShareAPI(&baseURL,
|
||||
ShareHandlers{
|
||||
ShareRequestCall: s.calls.shareCallRoute,
|
||||
ShareRequestCallDL: s.calls.shareCallDLRoute,
|
||||
ShareRequestIncident: s.incidents.getIncident,
|
||||
ShareRequestIncidentM3U: s.incidents.getCallsM3U,
|
||||
ShareRequestTalkgroups: s.tgs.getTGsShareRoute,
|
||||
},
|
||||
)
|
||||
|
||||
return s
|
||||
}
|
||||
|
@ -34,11 +64,11 @@ func New(baseURL url.URL) *api {
|
|||
func (a *api) Subrouter() http.Handler {
|
||||
r := chi.NewMux()
|
||||
|
||||
r.Mount("/talkgroup", new(talkgroupAPI).Subrouter())
|
||||
r.Mount("/call", new(callsAPI).Subrouter())
|
||||
r.Mount("/user", new(usersAPI).Subrouter())
|
||||
r.Mount("/incident", newIncidentsAPI(&a.baseURL).Subrouter())
|
||||
r.Mount("/share", newShareHandler(&a.baseURL).Subrouter())
|
||||
r.Mount("/talkgroup", a.tgs.Subrouter())
|
||||
r.Mount("/user", a.users.Subrouter())
|
||||
r.Mount("/call", a.calls.Subrouter())
|
||||
r.Mount("/incident", a.incidents.Subrouter())
|
||||
r.Mount("/share", a.shares.Subrouter())
|
||||
|
||||
return r
|
||||
}
|
||||
|
@ -141,6 +171,9 @@ var statusMapping = map[error]errResponder{
|
|||
ErrBadAppName: unauthErrText,
|
||||
common.ErrPageOutOfRange: badRequestErrText,
|
||||
rbac.ErrNotAuthorized: unauthErrText,
|
||||
shares.ErrNoShare: notFoundErrText,
|
||||
ErrBadShare: notFoundErrText,
|
||||
shares.ErrBadType: badRequestErrText,
|
||||
}
|
||||
|
||||
func autoError(err error) render.Renderer {
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"dynatron.me/x/stillbox/internal/forms"
|
||||
"dynatron.me/x/stillbox/pkg/calls/callstore"
|
||||
"dynatron.me/x/stillbox/pkg/database"
|
||||
"dynatron.me/x/stillbox/pkg/shares"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
|
@ -30,19 +31,20 @@ type callsAPI struct {
|
|||
func (ca *callsAPI) Subrouter() http.Handler {
|
||||
r := chi.NewMux()
|
||||
|
||||
r.Get(`/{call:[a-f0-9-]+}`, ca.getAudio)
|
||||
r.Get(`/{call:[a-f0-9-]+}/{download:download}`, ca.getAudio)
|
||||
|
||||
r.Get(`/{call:[a-f0-9-]+}`, ca.getAudioRoute)
|
||||
r.Get(`/{call:[a-f0-9-]+}/{download:download}`, ca.getAudioRoute)
|
||||
r.Post(`/`, ca.listCalls)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (ca *callsAPI) getAudio(w http.ResponseWriter, r *http.Request) {
|
||||
p := struct {
|
||||
CallID *uuid.UUID `param:"call"`
|
||||
Download *string `param:"download"`
|
||||
}{}
|
||||
type getAudioParams struct {
|
||||
CallID *uuid.UUID `param:"call"`
|
||||
Download *string `param:"download"`
|
||||
}
|
||||
|
||||
func (ca *callsAPI) getAudioRoute(w http.ResponseWriter, r *http.Request) {
|
||||
p := getAudioParams{}
|
||||
|
||||
err := decodeParams(&p, r)
|
||||
if err != nil {
|
||||
|
@ -50,6 +52,10 @@ func (ca *callsAPI) getAudio(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
ca.getAudio(p, w, r)
|
||||
}
|
||||
|
||||
func (ca *callsAPI) getAudio(p getAudioParams, w http.ResponseWriter, r *http.Request) {
|
||||
if p.CallID == nil {
|
||||
wErr(w, r, badRequest(ErrNoCall))
|
||||
return
|
||||
|
@ -96,6 +102,23 @@ func (ca *callsAPI) getAudio(w http.ResponseWriter, r *http.Request) {
|
|||
_, _ = w.Write(call.AudioBlob)
|
||||
}
|
||||
|
||||
func (ca *callsAPI) shareCallRoute(id ID, _ *shares.Share, w http.ResponseWriter, r *http.Request) {
|
||||
p := getAudioParams{
|
||||
CallID: common.PtrTo(id.(uuid.UUID)),
|
||||
}
|
||||
|
||||
ca.getAudio(p, w, r)
|
||||
}
|
||||
|
||||
func (ca *callsAPI) shareCallDLRoute(id ID, _ *shares.Share, w http.ResponseWriter, r *http.Request) {
|
||||
p := getAudioParams{
|
||||
CallID: common.PtrTo(id.(uuid.UUID)),
|
||||
Download: common.PtrTo("download"),
|
||||
}
|
||||
|
||||
ca.getAudio(p, w, r)
|
||||
}
|
||||
|
||||
func (ca *callsAPI) listCalls(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
cSt := callstore.FromCtx(ctx)
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"dynatron.me/x/stillbox/internal/jsontypes"
|
||||
"dynatron.me/x/stillbox/pkg/incidents"
|
||||
"dynatron.me/x/stillbox/pkg/incidents/incstore"
|
||||
"dynatron.me/x/stillbox/pkg/shares"
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
@ -22,16 +23,15 @@ type incidentsAPI struct {
|
|||
baseURL *url.URL
|
||||
}
|
||||
|
||||
func newIncidentsAPI(baseURL *url.URL) API {
|
||||
func newIncidentsAPI(baseURL *url.URL) *incidentsAPI {
|
||||
return &incidentsAPI{baseURL}
|
||||
}
|
||||
|
||||
func (ia *incidentsAPI) Subrouter() http.Handler {
|
||||
r := chi.NewMux()
|
||||
|
||||
r.Get(`/{id:[a-f0-9-]+}`, ia.getIncident)
|
||||
r.Get(`/{id:[a-f0-9-]+}.m3u`, ia.getCallsM3U)
|
||||
|
||||
r.Get(`/{id:[a-f0-9-]+}`, ia.getIncidentRoute)
|
||||
r.Get(`/{id:[a-f0-9-]+}.m3u`, ia.getCallsM3URoute)
|
||||
r.Post(`/new`, ia.createIncident)
|
||||
r.Post(`/`, ia.listIncidents)
|
||||
r.Post(`/{id:[a-f0-9-]+}/calls`, ia.postCalls)
|
||||
|
@ -88,16 +88,19 @@ func (ia *incidentsAPI) createIncident(w http.ResponseWriter, r *http.Request) {
|
|||
respond(w, r, inc)
|
||||
}
|
||||
|
||||
func (ia *incidentsAPI) getIncident(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
incs := incstore.FromCtx(ctx)
|
||||
|
||||
func (ia *incidentsAPI) getIncidentRoute(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := idOnlyParam(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
inc, err := incs.Incident(ctx, id)
|
||||
ia.getIncident(id, nil, w, r)
|
||||
}
|
||||
|
||||
func (ia *incidentsAPI) getIncident(id ID, share *shares.Share, w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
incs := incstore.FromCtx(ctx)
|
||||
inc, err := incs.Incident(ctx, id.(uuid.UUID))
|
||||
if err != nil {
|
||||
wErr(w, r, autoError(err))
|
||||
return
|
||||
|
@ -186,17 +189,21 @@ func (ia *incidentsAPI) postCalls(w http.ResponseWriter, r *http.Request) {
|
|||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (ia *incidentsAPI) getCallsM3U(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
incs := incstore.FromCtx(ctx)
|
||||
tgst := tgstore.FromCtx(ctx)
|
||||
|
||||
func (ia *incidentsAPI) getCallsM3URoute(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := idOnlyParam(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
inc, err := incs.Incident(ctx, id)
|
||||
ia.getCallsM3U(id, nil, w, r)
|
||||
}
|
||||
|
||||
func (ia *incidentsAPI) getCallsM3U(id ID, share *shares.Share, w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
incs := incstore.FromCtx(ctx)
|
||||
tgst := tgstore.FromCtx(ctx)
|
||||
|
||||
inc, err := incs.Incident(ctx, id.(uuid.UUID))
|
||||
if err != nil {
|
||||
wErr(w, r, autoError(err))
|
||||
return
|
||||
|
@ -205,6 +212,10 @@ func (ia *incidentsAPI) getCallsM3U(w http.ResponseWriter, r *http.Request) {
|
|||
b := new(bytes.Buffer)
|
||||
|
||||
callUrl := common.PtrTo(*ia.baseURL)
|
||||
urlRoot := "/api/call"
|
||||
if share != nil {
|
||||
urlRoot = fmt.Sprintf("/share/%s/call/", share.ID)
|
||||
}
|
||||
|
||||
b.WriteString("#EXTM3U\n\n")
|
||||
for _, c := range inc.Calls {
|
||||
|
@ -218,7 +229,7 @@ func (ia *incidentsAPI) getCallsM3U(w http.ResponseWriter, r *http.Request) {
|
|||
from = fmt.Sprintf(" from %d", c.Source)
|
||||
}
|
||||
|
||||
callUrl.Path = "/api/call/" + c.ID.String()
|
||||
callUrl.Path = urlRoot + c.ID.String()
|
||||
|
||||
fmt.Fprintf(b, "#EXTINF:%d,%s%s (%s)\n%s\n\n",
|
||||
c.Duration.Seconds(),
|
||||
|
|
|
@ -1,231 +1,164 @@
|
|||
package rest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"dynatron.me/x/stillbox/internal/common"
|
||||
"dynatron.me/x/stillbox/internal/forms"
|
||||
"dynatron.me/x/stillbox/internal/jsontypes"
|
||||
"dynatron.me/x/stillbox/pkg/incidents"
|
||||
"dynatron.me/x/stillbox/pkg/incidents/incstore"
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
|
||||
"dynatron.me/x/stillbox/pkg/rbac/entities"
|
||||
"dynatron.me/x/stillbox/pkg/shares"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrBadShare = errors.New("bad share request type")
|
||||
)
|
||||
|
||||
type ShareRequestType string
|
||||
|
||||
const (
|
||||
ShareRequestCall ShareRequestType = "call"
|
||||
ShareRequestCallDL ShareRequestType = "callDL"
|
||||
ShareRequestIncident ShareRequestType = "incident"
|
||||
ShareRequestIncidentM3U ShareRequestType = "m3u"
|
||||
ShareRequestTalkgroups ShareRequestType = "talkgroups"
|
||||
)
|
||||
|
||||
func (rt ShareRequestType) IsValid() bool {
|
||||
switch rt {
|
||||
case ShareRequestCall, ShareRequestCallDL, ShareRequestIncident,
|
||||
ShareRequestIncidentM3U, ShareRequestTalkgroups:
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (rt ShareRequestType) IsValidSubtype() bool {
|
||||
switch rt {
|
||||
case ShareRequestCall, ShareRequestCallDL, ShareRequestTalkgroups:
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
type ID interface {
|
||||
}
|
||||
|
||||
type HandlerFunc func(id ID, share *shares.Share, w http.ResponseWriter, r *http.Request)
|
||||
type ShareHandlers map[ShareRequestType]HandlerFunc
|
||||
type shareAPI struct {
|
||||
baseURL *url.URL
|
||||
shnd ShareHandlers
|
||||
}
|
||||
|
||||
func newShareHandler(baseURL *url.URL) API {
|
||||
return &shareAPI{baseURL}
|
||||
func newShareAPI(baseURL *url.URL, shnd ShareHandlers) *shareAPI {
|
||||
return &shareAPI{
|
||||
baseURL: baseURL,
|
||||
shnd: shnd,
|
||||
}
|
||||
}
|
||||
|
||||
func (ia *shareAPI) Subrouter() http.Handler {
|
||||
func (sa *shareAPI) Subrouter() http.Handler {
|
||||
r := chi.NewMux()
|
||||
|
||||
//r.Get(`/{id:[A-Za-z0-9_-]{20,}}`, ia.getShare)
|
||||
//r.Post('/create', ia.createShare)
|
||||
//r.Delete(`/{id:[A-Za-z0-9_-]{20,}}`, ia.deleteShare)
|
||||
//r.Get(`/`, ia.getShares)
|
||||
r.Post(`/create`, sa.createShare)
|
||||
r.Delete(`/{id:[A-Za-z0-9_-]{20,}}`, sa.deleteShare)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (ia *shareAPI) listIncidents(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
incs := incstore.FromCtx(ctx)
|
||||
func (sa *shareAPI) RootRouter() http.Handler {
|
||||
r := chi.NewMux()
|
||||
|
||||
r.Get("/{shareId:[A-Za-z0-9_-]{20,}}/{type}", sa.routeShare)
|
||||
r.Get("/{shareId:[A-Za-z0-9_-]{20,}}/{type}/{subID}", sa.routeShare)
|
||||
return r
|
||||
}
|
||||
|
||||
func (sa *shareAPI) createShare(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
shs := shares.FromCtx(ctx)
|
||||
|
||||
p := shares.CreateShareParams{}
|
||||
|
||||
p := incstore.IncidentsParams{}
|
||||
err := forms.Unmarshal(r, &p, forms.WithTag("json"), forms.WithAcceptBlank(), forms.WithOmitEmpty())
|
||||
if err != nil {
|
||||
wErr(w, r, badRequest(err))
|
||||
return
|
||||
}
|
||||
|
||||
res := struct {
|
||||
Incidents []incstore.Incident `json:"incidents"`
|
||||
Count int `json:"count"`
|
||||
sh, err := shs.NewShare(ctx, p)
|
||||
if err != nil {
|
||||
wErr(w, r, autoError(err))
|
||||
return
|
||||
}
|
||||
|
||||
respond(w, r, sh)
|
||||
}
|
||||
|
||||
func (sa *shareAPI) routeShare(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
shs := shares.FromCtx(ctx)
|
||||
|
||||
params := struct {
|
||||
Type string `param:"type"`
|
||||
ID string `param:"shareId"`
|
||||
SubID *string `param:"subID"`
|
||||
}{}
|
||||
|
||||
res.Incidents, res.Count, err = incs.Incidents(ctx, p)
|
||||
err := decodeParams(¶ms, r)
|
||||
if err != nil {
|
||||
wErr(w, r, autoError(err))
|
||||
return
|
||||
}
|
||||
|
||||
respond(w, r, res)
|
||||
}
|
||||
rType := ShareRequestType(params.Type)
|
||||
id := params.ID
|
||||
|
||||
func (ia *shareAPI) createIncident(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
incs := incstore.FromCtx(ctx)
|
||||
|
||||
p := incidents.Incident{}
|
||||
err := forms.Unmarshal(r, &p, forms.WithTag("json"), forms.WithAcceptBlank(), forms.WithOmitEmpty())
|
||||
if err != nil {
|
||||
wErr(w, r, badRequest(err))
|
||||
if !rType.IsValid() {
|
||||
wErr(w, r, autoError(ErrBadShare))
|
||||
return
|
||||
}
|
||||
|
||||
inc, err := incs.CreateIncident(ctx, p)
|
||||
sh, err := shs.GetShare(ctx, id)
|
||||
if err != nil {
|
||||
wErr(w, r, autoError(err))
|
||||
return
|
||||
}
|
||||
|
||||
respond(w, r, inc)
|
||||
}
|
||||
|
||||
func (ia *shareAPI) getIncident(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
incs := incstore.FromCtx(ctx)
|
||||
|
||||
id, err := idOnlyParam(w, r)
|
||||
if err != nil {
|
||||
if sh.Expiration != nil && sh.Expiration.Time().Before(time.Now()) {
|
||||
wErr(w, r, autoError(shares.ErrNoShare))
|
||||
return
|
||||
}
|
||||
|
||||
inc, err := incs.Incident(ctx, id)
|
||||
if err != nil {
|
||||
wErr(w, r, autoError(err))
|
||||
return
|
||||
}
|
||||
ctx = entities.CtxWithSubject(ctx, sh)
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
respond(w, r, inc)
|
||||
}
|
||||
|
||||
func (ia *shareAPI) updateIncident(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
incs := incstore.FromCtx(ctx)
|
||||
|
||||
id, err := idOnlyParam(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
p := incstore.UpdateIncidentParams{}
|
||||
err = forms.Unmarshal(r, &p, forms.WithTag("json"), forms.WithAcceptBlank(), forms.WithOmitEmpty())
|
||||
if err != nil {
|
||||
wErr(w, r, badRequest(err))
|
||||
return
|
||||
}
|
||||
|
||||
inc, err := incs.UpdateIncident(ctx, id, p)
|
||||
if err != nil {
|
||||
wErr(w, r, autoError(err))
|
||||
return
|
||||
}
|
||||
|
||||
respond(w, r, inc)
|
||||
}
|
||||
|
||||
func (ia *shareAPI) deleteIncident(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
incs := incstore.FromCtx(ctx)
|
||||
|
||||
urlParams := struct {
|
||||
ID uuid.UUID `param:"id"`
|
||||
}{}
|
||||
|
||||
err := decodeParams(&urlParams, r)
|
||||
if err != nil {
|
||||
wErr(w, r, badRequest(err))
|
||||
return
|
||||
}
|
||||
|
||||
err = incs.DeleteIncident(ctx, urlParams.ID)
|
||||
if err != nil {
|
||||
wErr(w, r, autoError(err))
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
type CallIncidentParams2 struct {
|
||||
Add jsontypes.UUIDs `json:"add"`
|
||||
Notes json.RawMessage `json:"notes"`
|
||||
|
||||
Remove jsontypes.UUIDs `json:"remove"`
|
||||
}
|
||||
|
||||
func (ia *shareAPI) postCalls(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
incs := incstore.FromCtx(ctx)
|
||||
|
||||
id, err := idOnlyParam(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
p := CallIncidentParams2{}
|
||||
err = forms.Unmarshal(r, &p, forms.WithTag("json"), forms.WithAcceptBlank(), forms.WithOmitEmpty())
|
||||
if err != nil {
|
||||
wErr(w, r, badRequest(err))
|
||||
return
|
||||
}
|
||||
|
||||
err = incs.AddRemoveIncidentCalls(ctx, id, p.Add.UUIDs(), p.Notes, p.Remove.UUIDs())
|
||||
if err != nil {
|
||||
wErr(w, r, autoError(err))
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (ia *shareAPI) getCallsM3U(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
incs := incstore.FromCtx(ctx)
|
||||
tgst := tgstore.FromCtx(ctx)
|
||||
|
||||
id, err := idOnlyParam(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
inc, err := incs.Incident(ctx, id)
|
||||
if err != nil {
|
||||
wErr(w, r, autoError(err))
|
||||
return
|
||||
}
|
||||
|
||||
b := new(bytes.Buffer)
|
||||
|
||||
callUrl := common.PtrTo(*ia.baseURL)
|
||||
|
||||
b.WriteString("#EXTM3U\n\n")
|
||||
for _, c := range inc.Calls {
|
||||
tg, err := tgst.TG(ctx, c.TalkgroupTuple())
|
||||
if err != nil {
|
||||
wErr(w, r, autoError(err))
|
||||
switch rType {
|
||||
case ShareRequestTalkgroups:
|
||||
sa.shnd[rType](nil, sh, w, r)
|
||||
case ShareRequestCall, ShareRequestCallDL:
|
||||
if params.SubID == nil {
|
||||
wErr(w, r, autoError(ErrBadShare))
|
||||
return
|
||||
}
|
||||
var from string
|
||||
if c.Source != 0 {
|
||||
from = fmt.Sprintf(" from %d", c.Source)
|
||||
|
||||
subIDU, err := uuid.Parse(*params.SubID)
|
||||
if err != nil {
|
||||
wErr(w, r, badRequest(err))
|
||||
return
|
||||
}
|
||||
|
||||
callUrl.Path = "/api/call/" + c.ID.String()
|
||||
|
||||
fmt.Fprintf(b, "#EXTINF:%d,%s%s (%s)\n%s\n\n",
|
||||
c.Duration.Seconds(),
|
||||
tg.StringTag(true),
|
||||
from,
|
||||
c.DateTime.Format("15:04 01/02"),
|
||||
callUrl,
|
||||
)
|
||||
sa.shnd[rType](subIDU, sh, w, r)
|
||||
case ShareRequestIncident, ShareRequestIncidentM3U:
|
||||
sa.shnd[rType](sh.EntityID, sh, w, r)
|
||||
}
|
||||
|
||||
// Not a lot of agreement on which MIME type to use for non-HLS m3u,
|
||||
// let's hope this is good enough
|
||||
w.Header().Set("Content-Type", "audio/x-mpegurl")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = b.WriteTo(w)
|
||||
}
|
||||
|
||||
func (sa *shareAPI) deleteShare(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
|
|
@ -7,6 +7,8 @@ import (
|
|||
|
||||
"dynatron.me/x/stillbox/internal/forms"
|
||||
"dynatron.me/x/stillbox/pkg/database"
|
||||
"dynatron.me/x/stillbox/pkg/incidents/incstore"
|
||||
"dynatron.me/x/stillbox/pkg/shares"
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups"
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups/xport"
|
||||
|
@ -159,6 +161,30 @@ func (tga *talkgroupAPI) postPaginated(w http.ResponseWriter, r *http.Request) {
|
|||
respond(w, r, res)
|
||||
}
|
||||
|
||||
func (tga *talkgroupAPI) getTGsShareRoute(_ ID, share *shares.Share, w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
tgs := tgstore.FromCtx(ctx)
|
||||
|
||||
tgIDs, err := incstore.FromCtx(ctx).TGsIn(ctx, share.EntityID)
|
||||
if err != nil {
|
||||
wErr(w, r, autoError(err))
|
||||
return
|
||||
}
|
||||
|
||||
idSl := make(talkgroups.IDs, 0, len(tgIDs))
|
||||
for id := range tgIDs {
|
||||
idSl = append(idSl, id)
|
||||
}
|
||||
|
||||
tgRes, err := tgs.TGs(ctx, idSl)
|
||||
if err != nil {
|
||||
wErr(w, r, autoError(err))
|
||||
return
|
||||
}
|
||||
|
||||
respond(w, r, tgRes)
|
||||
}
|
||||
|
||||
func (tga *talkgroupAPI) put(w http.ResponseWriter, r *http.Request) {
|
||||
var id tgParams
|
||||
err := decodeParams(&id, r)
|
||||
|
|
|
@ -51,7 +51,7 @@ func (s *Server) setupRoutes() {
|
|||
s.rateLimit(r)
|
||||
r.Use(render.SetContentType(render.ContentTypeJSON))
|
||||
s.auth.PublicRoutes(r)
|
||||
// r.Mount("/share", s.share.ShareRouter(s.rest))
|
||||
r.Mount("/share", s.rest.ShareRouter())
|
||||
})
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
|
|
|
@ -2,6 +2,7 @@ package server
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
@ -16,8 +17,9 @@ 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/share"
|
||||
"dynatron.me/x/stillbox/pkg/shares"
|
||||
"dynatron.me/x/stillbox/pkg/sinks"
|
||||
"dynatron.me/x/stillbox/pkg/sources"
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
|
||||
|
@ -45,12 +47,12 @@ type Server struct {
|
|||
notifier notify.Notifier
|
||||
hup chan os.Signal
|
||||
tgs tgstore.Store
|
||||
rest rest.API
|
||||
rest rest.APIRoot
|
||||
partman partman.PartitionManager
|
||||
users users.Store
|
||||
calls callstore.Store
|
||||
incidents incstore.Store
|
||||
share share.Service
|
||||
share shares.Service
|
||||
rbac rbac.RBAC
|
||||
}
|
||||
|
||||
|
@ -79,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
|
||||
}
|
||||
|
@ -96,7 +98,7 @@ func New(ctx context.Context, cfg *config.Configuration) (*Server, error) {
|
|||
tgs: tgCache,
|
||||
sinks: sinks.NewSinkManager(),
|
||||
rest: api,
|
||||
share: share.NewService(),
|
||||
share: shares.NewService(),
|
||||
users: ust,
|
||||
calls: callstore.NewStore(db),
|
||||
incidents: incstore.NewStore(),
|
||||
|
@ -145,6 +147,13 @@ func New(ctx context.Context, cfg *config.Configuration) (*Server, error) {
|
|||
}))
|
||||
srv.setupRoutes()
|
||||
|
||||
if os.Getenv("STILLBOX_DUMP_ROUTES") == "true" {
|
||||
_ = chi.Walk(r, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
|
||||
fmt.Printf("[%s]: '%s' has %d middlewares\n", method, route, len(middlewares))
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
return srv, nil
|
||||
}
|
||||
|
||||
|
@ -154,7 +163,7 @@ func (s *Server) fillCtx(ctx context.Context) context.Context {
|
|||
ctx = users.CtxWithStore(ctx, s.users)
|
||||
ctx = callstore.CtxWithStore(ctx, s.calls)
|
||||
ctx = incstore.CtxWithStore(ctx, s.incidents)
|
||||
ctx = share.CtxWithStore(ctx, s.share.ShareStore())
|
||||
ctx = shares.CtxWithStore(ctx, s.share)
|
||||
ctx = rbac.CtxWithRBAC(ctx, s.rbac)
|
||||
|
||||
return ctx
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
package share
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"dynatron.me/x/stillbox/internal/jsontypes"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/matoous/go-nanoid"
|
||||
)
|
||||
|
||||
const (
|
||||
SlugLength = 20
|
||||
)
|
||||
|
||||
type EntityType string
|
||||
|
||||
const (
|
||||
EntityIncident EntityType = "incident"
|
||||
EntityCall EntityType = "call"
|
||||
)
|
||||
|
||||
// If an incident is shared, all calls that are part of it must be shared too, but this can be through the incident share (/share/bLaH/callID[.mp3])
|
||||
|
||||
type Share struct {
|
||||
ID string `json:"id"`
|
||||
Type EntityType `json:"entityType"`
|
||||
EntityID uuid.UUID `json:"entityID"`
|
||||
Expiration *jsontypes.Time `json:"expiration"`
|
||||
}
|
||||
|
||||
// NewShare creates a new share.
|
||||
func (s *service) NewShare(ctx context.Context, shType EntityType, shID uuid.UUID, exp *time.Duration) (id string, err error) {
|
||||
id, err = gonanoid.ID(SlugLength)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
store := FromCtx(ctx)
|
||||
|
||||
var expT *jsontypes.Time
|
||||
if exp != nil {
|
||||
tt := time.Now().Add(*exp)
|
||||
expT = (*jsontypes.Time)(&tt)
|
||||
}
|
||||
|
||||
share := &Share{
|
||||
ID: id,
|
||||
Type: shType,
|
||||
EntityID: shID,
|
||||
Expiration: expT,
|
||||
}
|
||||
|
||||
err = store.Create(ctx, share)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
package share
|
||||
package shares
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"dynatron.me/x/stillbox/pkg/rbac"
|
||||
"dynatron.me/x/stillbox/pkg/rbac/entities"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
|
@ -13,21 +13,17 @@ const (
|
|||
)
|
||||
|
||||
type Service interface {
|
||||
ShareStore() Store
|
||||
Shares
|
||||
|
||||
Go(ctx context.Context)
|
||||
}
|
||||
|
||||
type service struct {
|
||||
Store
|
||||
}
|
||||
|
||||
func (s *service) ShareStore() Store {
|
||||
return s.Store
|
||||
postgresStore
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
|
@ -46,7 +42,5 @@ func (s *service) Go(ctx context.Context) {
|
|||
}
|
||||
|
||||
func NewService() *service {
|
||||
return &service{
|
||||
Store: NewStore(),
|
||||
}
|
||||
return &service{}
|
||||
}
|
144
pkg/shares/share.go
Normal file
144
pkg/shares/share.go
Normal file
|
@ -0,0 +1,144 @@
|
|||
package shares
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"dynatron.me/x/stillbox/internal/common"
|
||||
"dynatron.me/x/stillbox/internal/jsontypes"
|
||||
"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"
|
||||
"github.com/matoous/go-nanoid"
|
||||
)
|
||||
|
||||
const (
|
||||
SlugLength = 20
|
||||
)
|
||||
|
||||
var (
|
||||
ErrExpirationTooSoon = errors.New("expiration too soon")
|
||||
ErrBadType = errors.New("bad share type")
|
||||
)
|
||||
|
||||
type EntityType string
|
||||
|
||||
const (
|
||||
EntityIncident EntityType = "incident"
|
||||
EntityCall EntityType = "call"
|
||||
)
|
||||
|
||||
func (et EntityType) IsValid() bool {
|
||||
switch et {
|
||||
case EntityCall, EntityIncident:
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// If an incident is shared, all calls that are part of it must be shared too, but this can be through the incident share (/share/bLaH/callID[.mp3])
|
||||
|
||||
type Share struct {
|
||||
ID string `json:"id"`
|
||||
Type EntityType `json:"entityType"`
|
||||
Date *jsontypes.Time `json:"-"` // we handle this for the user
|
||||
Owner users.UserID `json:"owner"`
|
||||
EntityID uuid.UUID `json:"entityID"`
|
||||
Expiration *jsontypes.Time `json:"expiration"`
|
||||
}
|
||||
|
||||
func (s *Share) GetName() string {
|
||||
return "SHARE:" + s.ID
|
||||
}
|
||||
|
||||
func (s *Share) GetRoles() []string {
|
||||
return []string{entities.RoleShareGuest}
|
||||
}
|
||||
|
||||
func (s *Share) GetResourceName() string {
|
||||
return entities.ResourceShare
|
||||
}
|
||||
|
||||
type CreateShareParams struct {
|
||||
Type EntityType `json:"entityType"`
|
||||
EntityID uuid.UUID `json:"entityID"`
|
||||
Expiration *jsontypes.Duration `json:"expiration"`
|
||||
}
|
||||
|
||||
func (s *service) checkEntity(ctx context.Context, sh *CreateShareParams) (*time.Time, error) {
|
||||
var t *time.Time
|
||||
switch sh.Type {
|
||||
case EntityCall:
|
||||
cs := callstore.FromCtx(ctx)
|
||||
// Call does RBAC for us
|
||||
call, err := cs.Call(ctx, sh.EntityID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t = &call.DateTime
|
||||
case EntityIncident:
|
||||
is := incstore.FromCtx(ctx)
|
||||
i, err := is.Owner(ctx, sh.EntityID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = rbac.Check(ctx, &i, rbac.WithActions(entities.ActionShare))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// NewShare creates a new share.
|
||||
func (s *service) NewShare(ctx context.Context, sh CreateShareParams) (*Share, error) {
|
||||
if !sh.Type.IsValid() {
|
||||
return nil, ErrBadType
|
||||
}
|
||||
|
||||
// entTime is only meaningful if we are a call
|
||||
entTime, err := s.checkEntity(ctx, &sh)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u, err := users.From(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
id, err := gonanoid.ID(SlugLength)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
share := &Share{
|
||||
ID: id,
|
||||
Type: sh.Type,
|
||||
Date: (*jsontypes.Time)(entTime),
|
||||
Owner: u.ID,
|
||||
EntityID: sh.EntityID,
|
||||
}
|
||||
|
||||
if sh.Expiration != nil {
|
||||
if sh.Expiration.Duration() < time.Minute {
|
||||
return nil, ErrExpirationTooSoon
|
||||
}
|
||||
share.Expiration = common.PtrTo(jsontypes.Time(time.Now().Add(sh.Expiration.Duration())))
|
||||
}
|
||||
|
||||
err = s.Create(ctx, share)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return share, nil
|
||||
}
|
|
@ -1,15 +1,23 @@
|
|||
package share
|
||||
package shares
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type Store interface {
|
||||
// Get retreives a share record.
|
||||
Get(ctx context.Context, id string) (*Share, error)
|
||||
type Shares interface {
|
||||
// NewShare creates a new share.
|
||||
NewShare(ctx context.Context, sh CreateShareParams) (*Share, error)
|
||||
|
||||
// Share retreives a share record.
|
||||
GetShare(ctx context.Context, id string) (*Share, error)
|
||||
|
||||
// Create stores a new share record.
|
||||
Create(ctx context.Context, share *Share) error
|
||||
|
@ -24,38 +32,59 @@ type Store interface {
|
|||
type postgresStore struct {
|
||||
}
|
||||
|
||||
var (
|
||||
ErrNoShare = errors.New("no such share")
|
||||
)
|
||||
|
||||
func recToShare(share database.Share) *Share {
|
||||
return &Share{
|
||||
ID: share.ID,
|
||||
Type: EntityType(share.EntityType),
|
||||
EntityID: share.EntityID,
|
||||
Date: jsontypes.TimePtrFromTSTZ(share.EntityDate),
|
||||
Expiration: jsontypes.TimePtrFromTSTZ(share.Expiration),
|
||||
Owner: users.UserID(share.Owner),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *postgresStore) Get(ctx context.Context, id string) (*Share, error) {
|
||||
func (s *postgresStore) GetShare(ctx context.Context, id string) (*Share, error) {
|
||||
db := database.FromCtx(ctx)
|
||||
rec, err := db.GetShare(ctx, id)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case nil:
|
||||
return recToShare(rec), nil
|
||||
case pgx.ErrNoRows:
|
||||
return nil, ErrNoShare
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return recToShare(rec), nil
|
||||
}
|
||||
|
||||
func (s *postgresStore) Create(ctx context.Context, share *Share) error {
|
||||
sub, err := users.UserCheck(ctx, new(Share), "create")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
db := database.FromCtx(ctx)
|
||||
err := db.CreateShare(ctx, database.CreateShareParams{
|
||||
err = db.CreateShare(ctx, database.CreateShareParams{
|
||||
ID: share.ID,
|
||||
EntityType: string(share.Type),
|
||||
EntityID: share.EntityID,
|
||||
EntityDate: share.Date.PGTypeTSTZ(),
|
||||
Expiration: share.Expiration.PGTypeTSTZ(),
|
||||
Owner: sub.ID.Int(),
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *postgresStore) Delete(ctx context.Context, id string) error {
|
||||
_, err := rbac.Check(ctx, new(Share), rbac.WithActions(entities.ActionDelete))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return database.FromCtx(ctx).DeleteShare(ctx, id)
|
||||
}
|
||||
|
||||
|
@ -71,14 +100,14 @@ type storeCtxKey string
|
|||
|
||||
const StoreCtxKey storeCtxKey = "store"
|
||||
|
||||
func CtxWithStore(ctx context.Context, s Store) context.Context {
|
||||
func CtxWithStore(ctx context.Context, s Shares) context.Context {
|
||||
return context.WithValue(ctx, StoreCtxKey, s)
|
||||
}
|
||||
|
||||
func FromCtx(ctx context.Context) Store {
|
||||
s, ok := ctx.Value(StoreCtxKey).(Store)
|
||||
func FromCtx(ctx context.Context) Shares {
|
||||
s, ok := ctx.Value(StoreCtxKey).(Shares)
|
||||
if !ok {
|
||||
return NewStore()
|
||||
panic("no shares store in context")
|
||||
}
|
||||
|
||||
return s
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
@ -41,6 +41,18 @@ type ID struct {
|
|||
Talkgroup uint32 `json:"tg"`
|
||||
}
|
||||
|
||||
type PresenceMap map[ID]struct{}
|
||||
|
||||
func (t PresenceMap) Has(id ID) bool {
|
||||
_, has := t[id]
|
||||
|
||||
return has
|
||||
}
|
||||
|
||||
func (t PresenceMap) Put(id ID) {
|
||||
t[id] = struct{}{}
|
||||
}
|
||||
|
||||
var _ encoding.TextUnmarshaler = (*ID)(nil)
|
||||
|
||||
var ErrBadTG = errors.New("bad talkgroup format")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
package users
|
||||
|
||||
import (
|
||||
"dynatron.me/x/stillbox/pkg/rbac"
|
||||
)
|
||||
|
||||
type ShareLinkGuest struct {
|
||||
ShareID string
|
||||
}
|
||||
|
||||
func (s *ShareLinkGuest) GetRoles() []string {
|
||||
return []string{rbac.RoleShareGuest}
|
||||
}
|
||||
|
||||
type Public struct {
|
||||
RemoteAddr string
|
||||
}
|
||||
|
||||
func (s *Public) GetRoles() []string {
|
||||
return []string{rbac.RolePublic}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -169,6 +169,7 @@ CREATE TABLE IF NOT EXISTS shares(
|
|||
id TEXT PRIMARY KEY,
|
||||
entity_type TEXT NOT NULL,
|
||||
entity_id UUID NOT NULL,
|
||||
entity_date TIMESTAMPTZ,
|
||||
owner INTEGER NOT NULL REFERENCES users(id),
|
||||
expiration TIMESTAMPTZ NULL
|
||||
);
|
||||
|
|
|
@ -162,3 +162,24 @@ DELETE FROM calls WHERE id = @id;
|
|||
|
||||
-- name: GetCallSubmitter :one
|
||||
SELECT submitter FROM calls WHERE id = @id;
|
||||
|
||||
-- name: GetCall :one
|
||||
SELECT
|
||||
id,
|
||||
submitter,
|
||||
system,
|
||||
talkgroup,
|
||||
call_date,
|
||||
audio_name,
|
||||
audio_type,
|
||||
audio_url,
|
||||
duration,
|
||||
frequency,
|
||||
frequencies,
|
||||
patches,
|
||||
tg_label,
|
||||
tg_alpha_tag,
|
||||
tg_group,
|
||||
source
|
||||
FROM calls
|
||||
WHERE id = @id;
|
||||
|
|
|
@ -29,6 +29,13 @@ UPDATE incidents_Calls
|
|||
SET notes = @notes
|
||||
WHERE incident_id = @incident_id AND call_id = @call_id;
|
||||
|
||||
-- name: CallInIncident :one
|
||||
SELECT EXISTS
|
||||
(SELECT 1 FROM incidents_calls ic
|
||||
WHERE
|
||||
ic.incident_id = @incident_id AND
|
||||
ic.call_id = @call_id);
|
||||
|
||||
-- name: CreateIncident :one
|
||||
INSERT INTO incidents (
|
||||
id,
|
||||
|
|
|
@ -3,6 +3,7 @@ SELECT
|
|||
id,
|
||||
entity_type,
|
||||
entity_id,
|
||||
entity_date,
|
||||
owner,
|
||||
expiration
|
||||
FROM shares
|
||||
|
@ -13,9 +14,10 @@ INSERT INTO shares (
|
|||
id,
|
||||
entity_type,
|
||||
entity_id,
|
||||
entity_date,
|
||||
owner,
|
||||
expiration
|
||||
) VALUES (@id, @entity_type, @entity_id, @owner, sqlc.narg('expiration'));
|
||||
) VALUES (@id, @entity_type, @entity_id, sqlc.narg('entity_date'), @owner, sqlc.narg('expiration'));
|
||||
|
||||
-- name: DeleteShare :exec
|
||||
DELETE FROM shares WHERE id = @id;
|
||||
|
|
|
@ -281,3 +281,11 @@ INSERT INTO systems(id, name) VALUES(@id, @name);
|
|||
|
||||
-- name: DeleteSystem :exec
|
||||
DELETE FROM systems WHERE id = @id;
|
||||
|
||||
-- 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;
|
||||
|
|
Loading…
Reference in a new issue